Skip to content

Commit b0d0726

Browse files
committed
Merge branch 'ben/matching-service' into frontend-websocket
2 parents 8668971 + 57c4fd9 commit b0d0726

File tree

10 files changed

+322
-86
lines changed

10 files changed

+322
-86
lines changed

apps/matching-service/.env.example

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

apps/matching-service/README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,15 @@ go mod tidy
2727
- `PORT`: Specifies the port for the WebSocket server. Default is `8081`.
2828
- `JWT_SECRET`: The secret key used to verify JWT tokens.
2929
- `MATCH_TIMEOUT`: The time in seconds to wait for a match before timing out.
30+
- `REDIS_URL`: The URL for the Redis server. Default is `localhost:6379`.
3031

31-
4. Start the WebSocket server:
32+
4. Start a local redis server:
33+
34+
```bash
35+
docker run -d -p 6379:6379 redis
36+
```
37+
38+
5. Start the WebSocket server:
3239

3340
```bash
3441
go run main.go
@@ -68,7 +75,9 @@ Client sends matching parameters:
6875
{
6976
"type": "match_request",
7077
"topics": ["Algorithms", "Arrays"],
71-
"difficulties": ["Easy", "Medium"]
78+
"difficulties": ["Easy", "Medium"],
79+
"username": "Jane Doe",
80+
"email": "[email protected]" // possible to change to user ID in mongodb
7281
}
7382
```
7483

@@ -77,9 +86,9 @@ Server response on successful match:
7786
```json
7887
{
7988
"type": "match_found",
80-
"matchID": 67890,
81-
"partnerID": 54321,
82-
"partnerName": "John Doe"
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
8392
}
8493
```
8594

@@ -100,6 +109,8 @@ Utilize `./tests/websocket-test.html` for a basic debugging interface of the mat
100109

101110
Make sure to open the HTML file in a web browser while the WebSocket server is running to perform your tests.
102111

112+
You can open one instance of the HTML file in multiple tabs to simulate multiple clients connecting to the server. (In the future: ensure that only one connection is allowed per user)
113+
103114
## Docker Support
104115

105116
TODO: Add section for Docker setup and usage instructions.

apps/matching-service/go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@ go 1.23.1
55
require (
66
github.com/gorilla/websocket v1.5.3
77
github.com/joho/godotenv v1.5.1
8+
github.com/redis/go-redis/v9 v9.6.2
9+
)
10+
11+
require (
12+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
13+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
814
)

apps/matching-service/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
2+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
3+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
4+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
5+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
6+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
7+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
8+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
19
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
210
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
311
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
412
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
13+
github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk=
14+
github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=

apps/matching-service/handlers/websocket.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func readMatchRequest(ws *websocket.Conn) (models.MatchRequest, error) {
6969
if err := ws.ReadJSON(&matchRequest); err != nil {
7070
return matchRequest, err
7171
}
72+
matchRequest.Port = ws.RemoteAddr().String()
7273
log.Printf("Received match request: %v", matchRequest)
7374
return matchRequest, nil
7475
}

apps/matching-service/main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"log"
67
"matching-service/handlers"
8+
"matching-service/processes"
79
"net/http"
810
"os"
911

1012
"github.com/joho/godotenv"
13+
"github.com/redis/go-redis/v9"
1114
)
1215

1316
func main() {
@@ -18,6 +21,25 @@ func main() {
1821
}
1922
port := os.Getenv("PORT")
2023

24+
// Set up link with redis server
25+
redisAddr := os.Getenv("REDIS_URL")
26+
client := redis.NewClient(&redis.Options{
27+
Addr: redisAddr,
28+
Password: "", // no password set
29+
DB: 0, // use default DB
30+
})
31+
32+
// Ping the redis server
33+
_, err = client.Ping(context.Background()).Result()
34+
if err != nil {
35+
log.Fatalf("Could not connect to Redis: %v", err)
36+
} else {
37+
log.Println("Connected to Redis at the following address: " + redisAddr)
38+
}
39+
40+
// Pass the connect redis client to processes in match
41+
processes.SetRedisClient(client)
42+
2143
// Routes
2244
http.HandleFunc("/match", handlers.HandleWebSocketConnections)
2345

apps/matching-service/models/match.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ type MatchRequest struct {
44
Type string `json:"type"`
55
Topics []string `json:"topics"`
66
Difficulties []string `json:"difficulties"`
7+
Username string `json:"username"`
8+
Email string `json:"email"`
9+
Port string `json:"port"`
710
}
811

912
type MatchFound struct {
1013
Type string `json:"type"`
11-
MatchID int64 `json:"matchId"`
14+
MatchID string `json:"matchId"`
1215
PartnerID int64 `json:"partnerId"`
1316
PartnerName string `json:"partnerName"`
1417
}

apps/matching-service/processes/match.go

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,87 @@ package processes
22

33
import (
44
"context"
5+
"fmt"
6+
"log"
57
"matching-service/models"
6-
"time"
8+
"strconv"
9+
"strings"
10+
"sync"
11+
12+
"github.com/redis/go-redis/v9"
713
)
814

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",
15+
var (
16+
redisClient *redis.Client
17+
mu sync.Mutex
18+
)
19+
20+
// SetRedisClient sets the Redis client to a global variable
21+
func SetRedisClient(client *redis.Client) {
22+
redisClient = client
23+
}
24+
25+
func getPortNumber(addr string) (int64, error) {
26+
// Split the string by the colon
27+
parts := strings.Split(addr, ":")
28+
if len(parts) < 2 {
29+
return 0, fmt.Errorf("no port number found")
30+
}
31+
32+
// Convert the port string to an integer
33+
port, err := strconv.ParseInt(parts[len(parts)-1], 10, 64)
34+
if err != nil {
35+
return 0, err // Return an error if conversion fails
3236
}
3337

34-
matchFoundChan <- result
38+
return port, nil
39+
}
40+
41+
func PerformMatching(matchRequest models.MatchRequest, ctx context.Context, matchFoundChan chan models.MatchFound) {
42+
defer close(matchFoundChan) // Safely close the channel after matching completes
43+
44+
for {
45+
select {
46+
case <-ctx.Done():
47+
// Cleaning up, dequeue the user
48+
err := dequeueUser(redisClient, matchRequest)
49+
if err != nil {
50+
log.Println("Failed to dequeue user:", err)
51+
}
52+
53+
return
54+
55+
default:
56+
// Continue matching logic...
57+
mu.Lock()
58+
match, err := matchUser(redisClient, matchRequest)
59+
mu.Unlock()
60+
if err != nil {
61+
log.Println("Error occurred during matching:", err)
62+
return
63+
}
64+
65+
if match != "" {
66+
arr := strings.Split(match, ",")
67+
username := arr[0]
68+
// email := arr[1]
69+
port := arr[2]
70+
matchId := arr[3]
71+
partnerPort, err := getPortNumber(port)
72+
if err != nil {
73+
log.Println("Error occurred while getting PartnerID:", err)
74+
return
75+
}
76+
77+
result := models.MatchFound{
78+
Type: "match_found",
79+
MatchID: matchId,
80+
PartnerID: partnerPort, // Use the retrieved PartnerID
81+
PartnerName: username,
82+
}
83+
matchFoundChan <- result
84+
return
85+
}
86+
}
87+
}
3588
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package processes
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"encoding/hex"
7+
"fmt"
8+
"matching-service/models"
9+
10+
"github.com/redis/go-redis/v9"
11+
)
12+
13+
var ctx = context.Background()
14+
15+
func generateMatchID() (string, error) {
16+
// Create a byte slice to hold random data
17+
b := make([]byte, 16) // 16 bytes = 128 bits
18+
_, err := rand.Read(b)
19+
if err != nil {
20+
return "", err
21+
}
22+
23+
// Encode the byte slice to a hexadecimal string
24+
matchID := hex.EncodeToString(b)
25+
return matchID, nil
26+
}
27+
28+
func enqueueUser(client *redis.Client, request models.MatchRequest) error {
29+
// Generate the key for storing in redis
30+
key := fmt.Sprintf("queue:%s:%s", request.Topics, request.Difficulties)
31+
32+
// Concatenate the username,email,port
33+
value := fmt.Sprintf("%s,%s,%s", request.Username, request.Email, request.Port)
34+
35+
// Push the user into the matching queue
36+
return client.LPush(ctx, key, value).Err()
37+
}
38+
39+
func dequeueUser(client *redis.Client, request models.MatchRequest) error {
40+
// Generate the key for storing in redis
41+
key := fmt.Sprintf("queue:%s:%s", request.Topics, request.Difficulties)
42+
43+
value := fmt.Sprintf("%s,%s,%s", request.Username, request.Email, request.Port)
44+
45+
// Remove user from the matching queue
46+
_, err := client.LRem(ctx, key, 1, value).Result()
47+
return err
48+
}
49+
50+
func matchUser(client *redis.Client, request models.MatchRequest) (string, error) {
51+
key := fmt.Sprintf("queue:%s:%s", request.Topics, request.Difficulties)
52+
value := fmt.Sprintf("%s,%s,%s", request.Username, request.Email, request.Port)
53+
54+
// Check if the user is already matched
55+
exists, err := client.HExists(ctx, value, "username").Result()
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
if exists {
61+
// User is already matched, retrieve their details
62+
matchedUser, err := client.HGetAll(ctx, value).Result()
63+
if err != nil {
64+
return "", err
65+
}
66+
67+
// Remove from Redis once retrieved
68+
_, err = client.Del(ctx, value).Result()
69+
if err != nil {
70+
return "", err
71+
}
72+
73+
return fmt.Sprintf("%s,%s,%s,%s", matchedUser["username"], matchedUser["email"], matchedUser["port"], matchedUser["matchid"]), nil
74+
}
75+
76+
match, err := client.RPop(ctx, key).Result()
77+
78+
if err == redis.Nil {
79+
// No match found, enqueue user and return nil
80+
enqueueUser(client, request)
81+
return "", nil
82+
} else if err != nil {
83+
return "", err
84+
}
85+
86+
// Check if the matched user is the same as the requesting user
87+
if match == value {
88+
// If matched user is the same, you can push it back to the queue and try again
89+
client.LPush(ctx, key, match) // Re-add the matched user back to the queue
90+
return "", nil
91+
}
92+
93+
// Randomly create a matchid
94+
matchID, err := generateMatchID()
95+
if err != nil {
96+
fmt.Println("Error generating match ID:", err)
97+
return "", err
98+
}
99+
100+
// Store matched user details in a hash
101+
err = client.HSet(ctx, match, map[string]interface{}{
102+
"username": request.Username,
103+
"email": request.Email,
104+
"port": request.Port,
105+
"matchid": matchID,
106+
}).Err()
107+
if err != nil {
108+
return "", err
109+
}
110+
111+
return fmt.Sprintf("%s,%s", match, matchID), nil
112+
}

0 commit comments

Comments
 (0)