Skip to content

Commit f4a8c7b

Browse files
committed
feat: set up basic matching service websocket
1 parent 250acac commit f4a8c7b

File tree

12 files changed

+345
-16
lines changed

12 files changed

+345
-16
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: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ The Matching Service provides a WebSocket server to manage real-time matching re
44

55
## Setup
66

7+
### Prerequisites
8+
9+
Ensure you have Go installed on your machine.
10+
11+
### Installation
12+
713
1. Navigate to the matching service directory:
814

915
```bash
1016
cd ./apps/matching-service
1117
```
1218

13-
2. Ensure that you have Go installed. Install the necessary dependencies:
19+
2. Install the necessary dependencies:
1420

1521
```bash
1622
go mod tidy
@@ -27,7 +33,7 @@ go run main.go
2733
To establish a WebSocket connection with the matching service, use the following JavaScript code:
2834

2935
```javascript
30-
const ws = new WebSocket("ws://localhost:8081/ws");
36+
const ws = new WebSocket("ws://localhost:8081/match");
3137
```
3238

3339
### Authentication
@@ -65,8 +71,8 @@ Server response on successful match:
6571
```json
6672
{
6773
"type": "match_found",
68-
"matchID": "67890",
69-
"partnerID": "54321",
74+
"matchID": 67890,
75+
"partnerID": 54321,
7076
"partnerName": "John Doe"
7177
}
7278
```
@@ -80,21 +86,20 @@ If no match is found after a set period of time, the server will send a timeout
8086
}
8187
```
8288

83-
If the server encounters an issue during the WebSocket connection or processing, an error message will be sent back to the client before closing the connection. The client should handle these errors appropriately.
84-
85-
Sample error structure:
86-
87-
```json
88-
{
89-
"type": "error",
90-
"message": "Invalid token"
91-
}
92-
```
89+
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.
9390

9491
### Environment Variables
9592

9693
- `PORT`: Specifies the port for the WebSocket server. Default is `8081`.
9794
- `JWT_SECRET`: The secret key used to verify JWT tokens.
98-
- `MATCH_TIMEOUT`: The time in seconds to wait for a match before timing out. Default is `60` seconds.
95+
- `MATCH_TIMEOUT`: The time in seconds to wait for a match before timing out.
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
99104

100-
TODO: Add section for docker
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 matcghing-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+
"matcghing-service/models"
7+
"matcghing-service/processes"
8+
"matcghing-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+
"matcghing-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+
"matcghing-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)