Skip to content

Commit 408dbb8

Browse files
authored
Merge pull request #37 from CS3219-AY2425S1/titus/setup-matching-service
feat: setup matching service backend
2 parents 098224f + 3e4caa6 commit 408dbb8

File tree

12 files changed

+429
-0
lines changed

12 files changed

+429
-0
lines changed

apps/matching-service/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
PORT=8081
2+
MATCH_TIMEOUT=3
3+
JWT_SECRET=you-can-replace-this-with-your-own-secret

apps/matching-service/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

apps/matching-service/README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Matching Service
2+
3+
The Matching Service provides a WebSocket server to manage real-time matching requests through WebSocket connections. It is implemented in Go and uses the Gorilla WebSocket package to facilitate communication.
4+
5+
## Setup
6+
7+
### Prerequisites
8+
9+
Ensure you have Go installed on your machine.
10+
11+
### Installation
12+
13+
1. Navigate to the matching service directory:
14+
15+
```bash
16+
cd ./apps/matching-service
17+
```
18+
19+
2. Install the necessary dependencies:
20+
21+
```bash
22+
go mod tidy
23+
```
24+
25+
3. Create a copy of the `.env.example` file as an `.env` file with the following environment variables:
26+
27+
- `PORT`: Specifies the port for the WebSocket server. Default is `8081`.
28+
- `JWT_SECRET`: The secret key used to verify JWT tokens.
29+
- `MATCH_TIMEOUT`: The time in seconds to wait for a match before timing out.
30+
31+
4. Start the WebSocket server:
32+
33+
```bash
34+
go run main.go
35+
```
36+
37+
## API Usage
38+
39+
To establish a WebSocket connection with the matching service, use the following JavaScript code:
40+
41+
```javascript
42+
const ws = new WebSocket("ws://localhost:8081/match");
43+
```
44+
45+
### Authentication
46+
47+
The initial WebSocket request should include a JWT token that contains the user’s ID as a claim. This token will be verified by the server to authenticate the user. The user ID extracted is used to identify the client and facilitate the matching process.
48+
49+
### Matching Workflow
50+
51+
1. **Sending Matching Parameter**s: Once the WebSocket connection is established, the client sends a message containing the matching parameters (e.g., preferred topics or difficulty levels).
52+
53+
2. **Receiving Match Results**:
54+
2.1. **Match Found**: If a match is found, the server sends the matching result back to the client via the WebSocket connection.
55+
2.2. **No Match Found**: If after a set amount of time, no match is found, the request timeouts, and the server sends a message that the matching failed.
56+
57+
3. **Connection Closure**:
58+
3.1. **Received Match Result**: After sending the match result, the server closes the connection.
59+
3.2. **Cancellation**: If the user cancels the matching process, the client should close the connection. The server will recognize this and cancel the ongoing match.
60+
61+
### Message Formats
62+
63+
Provide the structure of the messages being sent back and forth between the server and the client. This includes the format of the initial matching parameters and the response payload. All requests should be in JSON and contain the `type` field to handle different types of messages.
64+
65+
Client sends matching parameters:
66+
67+
```json
68+
{
69+
"type": "match_request",
70+
"topics": ["Algorithms", "Arrays"],
71+
"difficulties": ["Easy", "Medium"]
72+
}
73+
```
74+
75+
Server response on successful match:
76+
77+
```json
78+
{
79+
"type": "match_found",
80+
"matchID": 67890,
81+
"partnerID": 54321,
82+
"partnerName": "John Doe"
83+
}
84+
```
85+
86+
If no match is found after a set period of time, the server will send a timeout message:
87+
88+
```json
89+
{
90+
"type": "timeout",
91+
"message": "No match found. Please try again later."
92+
}
93+
```
94+
95+
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.
96+
97+
## Testing
98+
99+
Utilize `./tests/websocket-test.html` for a basic debugging interface of the matching service. This HTML file provides an interactive way to test the WebSocket connection, send matching requests, and observe responses from the server.
100+
101+
Make sure to open the HTML file in a web browser while the WebSocket server is running to perform your tests.
102+
103+
## Docker Support
104+
105+
TODO: Add section for Docker setup and usage instructions.

apps/matching-service/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module matching-service
2+
3+
go 1.23.1
4+
5+
require (
6+
github.com/gorilla/websocket v1.5.3
7+
github.com/joho/godotenv v1.5.1
8+
)

apps/matching-service/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
2+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
3+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
4+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"log"
6+
"matching-service/models"
7+
"matching-service/processes"
8+
"matching-service/utils"
9+
"net/http"
10+
11+
"github.com/gorilla/websocket"
12+
)
13+
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+
}
20+
21+
// handleConnections manages WebSocket connections and matching logic.
22+
func HandleWebSocketConnections(w http.ResponseWriter, r *http.Request) {
23+
// TODO: Parse the authorization header to validate the JWT token and get the user ID claim.
24+
25+
ws, err := upgrader.Upgrade(w, r, nil)
26+
if err != nil {
27+
log.Fatal(err)
28+
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
29+
return
30+
}
31+
defer func() {
32+
if closeErr := ws.Close(); closeErr != nil {
33+
log.Printf("WebSocket close error: %v", closeErr)
34+
}
35+
}()
36+
37+
log.Println("WebSocket connection established")
38+
39+
matchRequest, err := readMatchRequest(ws)
40+
if err != nil {
41+
log.Printf("Error reading match request: %v", err)
42+
return
43+
}
44+
45+
// Create a context for cancellation
46+
ctx, cancel := context.WithCancel(context.Background())
47+
defer cancel() // Ensure cancel is called to release resources
48+
49+
timeoutCtx, timeoutCancel, err := createTimeoutContext()
50+
if err != nil {
51+
log.Printf("Error creating timeout context: %v", err)
52+
return
53+
}
54+
defer timeoutCancel()
55+
56+
matchFoundChan := make(chan models.MatchFound)
57+
58+
// Start goroutines for handling messages and performing matching.
59+
go processes.ReadMessages(ws, ctx, cancel)
60+
go processes.PerformMatching(matchRequest, ctx, matchFoundChan) // Perform matching
61+
62+
// Wait for a match, timeout, or cancellation.
63+
waitForResult(ws, ctx, timeoutCtx, matchFoundChan)
64+
}
65+
66+
// readMatchRequest reads the initial match request from the WebSocket connection.
67+
func readMatchRequest(ws *websocket.Conn) (models.MatchRequest, error) {
68+
var matchRequest models.MatchRequest
69+
if err := ws.ReadJSON(&matchRequest); err != nil {
70+
return matchRequest, err
71+
}
72+
log.Printf("Received match request: %v", matchRequest)
73+
return matchRequest, nil
74+
}
75+
76+
// createTimeoutContext sets up a timeout context based on configuration.
77+
func createTimeoutContext() (context.Context, context.CancelFunc, error) {
78+
timeoutDuration, err := utils.GetTimeoutDuration()
79+
if err != nil {
80+
return nil, nil, err
81+
}
82+
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
83+
return ctx, cancel, nil
84+
}
85+
86+
// waitForResult waits for a match result, timeout, or cancellation.
87+
func waitForResult(ws *websocket.Conn, ctx, timeoutCtx context.Context, matchFoundChan chan models.MatchFound) {
88+
select {
89+
case <-ctx.Done():
90+
log.Println("Matching cancelled")
91+
return
92+
case <-timeoutCtx.Done():
93+
log.Println("Connection timed out")
94+
sendTimeoutResponse(ws)
95+
return
96+
case result, ok := <-matchFoundChan:
97+
if !ok {
98+
// Channel closed without a match, possibly due to context cancellation
99+
log.Println("Match channel closed without finding a match")
100+
return
101+
}
102+
log.Println("Match found")
103+
if err := ws.WriteJSON(result); err != nil {
104+
log.Printf("write error: %v", err)
105+
}
106+
return
107+
}
108+
}
109+
110+
// sendTimeoutResponse sends a timeout message to the WebSocket client.
111+
func sendTimeoutResponse(ws *websocket.Conn) {
112+
result := models.Timeout{
113+
Type: "timeout",
114+
Message: "No match found. Please try again later.",
115+
}
116+
if err := ws.WriteJSON(result); err != nil {
117+
log.Printf("write error: %v", err)
118+
}
119+
}

apps/matching-service/main.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"matching-service/handlers"
7+
"net/http"
8+
"os"
9+
10+
"github.com/joho/godotenv"
11+
)
12+
13+
func main() {
14+
// Set up environment
15+
err := godotenv.Load()
16+
if err != nil {
17+
log.Fatalf("err loading: %v", err)
18+
}
19+
port := os.Getenv("PORT")
20+
21+
// Routes
22+
http.HandleFunc("/match", handlers.HandleWebSocketConnections)
23+
24+
// Start the server
25+
log.Println(fmt.Sprintf("Server starting on :%s", port))
26+
err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
27+
if err != nil {
28+
log.Fatal("ListenAndServe: ", err)
29+
}
30+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package models
2+
3+
type MatchRequest struct {
4+
Type string `json:"type"`
5+
Topics []string `json:"topics"`
6+
Difficulties []string `json:"difficulties"`
7+
}
8+
9+
type MatchFound struct {
10+
Type string `json:"type"`
11+
MatchID int64 `json:"matchId"`
12+
PartnerID int64 `json:"partnerId"`
13+
PartnerName string `json:"partnerName"`
14+
}
15+
16+
type Timeout struct {
17+
Type string `json:"timeout"`
18+
Message string `json:"message"`
19+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package processes
2+
3+
import (
4+
"context"
5+
"matching-service/models"
6+
"time"
7+
)
8+
9+
// PerformMatching reads the match request and simulates a matching process.
10+
func PerformMatching(matchRequest models.MatchRequest, ctx context.Context, matchFoundChan chan models.MatchFound) {
11+
defer close(matchFoundChan) // Safely close the channel after matching completes.
12+
13+
// TODO: matching algorithm
14+
// for {
15+
// select {
16+
// case <-ctx.Done():
17+
// // The context has been cancelled, so stop the matching process.
18+
// return
19+
// default:
20+
// // Continue matching logic...
21+
// }
22+
// }
23+
// Simulate the matching process with a sleep (replace with actual logic).
24+
time.Sleep(2 * time.Second)
25+
26+
// Create a mock result and send it to the channel.
27+
result := models.MatchFound{
28+
Type: "match_found",
29+
MatchID: 67890,
30+
PartnerID: 54321,
31+
PartnerName: "John Doe",
32+
}
33+
34+
matchFoundChan <- result
35+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package processes
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/gorilla/websocket"
8+
)
9+
10+
// ReadMessages reads messages from the WebSocket and cancels on error.
11+
// This is primarily meant for detecting if the client cancels the matching
12+
func ReadMessages(ws *websocket.Conn, ctx context.Context, cancel func()) {
13+
for {
14+
_, _, err := ws.ReadMessage()
15+
if err != nil {
16+
log.Println("Connection closed or error:", err)
17+
cancel() //Cancel the context to terminate other goroutines
18+
return
19+
}
20+
// Optional: Reset any timeout here if needed.
21+
}
22+
}

0 commit comments

Comments
 (0)