add random match-making logic

This commit is contained in:
2026-09-21 22:40:35 +03:00
parent 30cf8c7215
commit 7db58e42ab
7 changed files with 163 additions and 1 deletions
+1 -1
View File
@@ -33,5 +33,5 @@
# Ignore key files for decrypting credentials and more.
/main-server/config/*.key
/frontend/node-modules
/frontend/node_modules
/frontend/dist
+2
View File
@@ -38,3 +38,5 @@ group :development, :test do
gem "brakeman", require: false
gem "rubocop-rails-omakase", require: false
end
gem "redis", "~> 6.0"
+7
View File
@@ -250,6 +250,10 @@ GEM
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis (6.0.0)
redis-client (= 0.30.1)
redis-client (0.30.1)
connection_pool
regexp_parser (2.12.0)
reline (0.7.0)
io-console (~> 0.5)
@@ -357,6 +361,7 @@ DEPENDENCIES
puma (>= 5.0)
rack-cors
rails (~> 8.1.2)
redis (~> 6.0)
rubocop-rails-omakase
solid_cable
solid_cache
@@ -459,6 +464,8 @@ CHECKSUMS
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
redis (6.0.0) sha256=de71c10edd106986b759ec7ecdd08b63b9c0ee7414a0d0c1da73d31ba2bccda6
redis-client (0.30.1) sha256=5151bc5c7bbfe48623732cdae3b900d8a22dc691cc7cdfacfb351ac55116522d
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0
@@ -0,0 +1,21 @@
class Api::MatchmakingController < ApplicationController
def create
result = MatchmakingService.find_match(current_user.id)
if result
render json: {
match_id: result[:match_id],
game_token: result[:game_token],
game_server: result[:game_server],
players: result[:players]
}
else
render json: { error: "No opponent found. Try again." }, status: :request_timeout
end
end
def destroy
MatchmakingService.cancel(current_user.id)
render json: { message: "Matchmaking cancelled" }
end
end
@@ -0,0 +1,122 @@
class MatchmakingService
QUEUE_KEY = "matchmaking:queue"
LOCK_KEY = "matchmaking:lock"
MATCH_PREFIX = "matchmaking:match:"
MATCH_TTL = 60 # seconds
POLL_INTERVAL = 0.5 # seconds
MAX_WAIT = 30 # seconds
# Game server to redirect players to (hardcoded for now)
GAME_SERVER = ENV.fetch("GAME_SERVER_URL", "ws://game-server.example.com:8080")
class << self
# Main entry point: waits for an opponent and returns match data
def find_match(user_id)
user_id = user_id.to_i
# Try to pair with an existing waiting player
opponent_id = pop_opponent(user_id)
if opponent_id
create_match(user_id, opponent_id)
else
# Add self to queue and wait
add_to_queue(user_id)
wait_for_match(user_id)
end
end
# Cancel matchmaking (remove from queue)
def cancel(user_id)
RedisConnection.with do |redis|
redis.lrem(QUEUE_KEY, 0, user_id)
end
end
private
# Atomically pop an opponent from the queue (excluding self)
def pop_opponent(user_id)
RedisConnection.with do |redis|
redis.watch(QUEUE_KEY) do
queue = redis.lrange(QUEUE_KEY, 0, -1)
opponent = queue.find { |id| id.to_i != user_id }
if opponent
result = redis.multi do |tx|
tx.lrem(QUEUE_KEY, 0, opponent)
end
return opponent.to_i if result&.first&.positive?
else
redis.unwatch
end
end
end
nil
end
def add_to_queue(user_id)
RedisConnection.with do |redis|
# Remove any stale entries first
redis.lrem(QUEUE_KEY, 0, user_id)
redis.rpush(QUEUE_KEY, user_id)
end
end
def wait_for_match(user_id)
start_time = Time.current
match_id = nil
while Time.current - start_time < MAX_WAIT
RedisConnection.with do |redis|
match_id = redis.get("#{MATCH_PREFIX}#{user_id}")
end
break if match_id
sleep POLL_INTERVAL
end
if match_id
# Retrieve and delete the match data
data = nil
RedisConnection.with do |redis|
data = redis.get("#{MATCH_PREFIX}match:#{match_id}")
redis.del("#{MATCH_PREFIX}#{user_id}")
redis.del("#{MATCH_PREFIX}match:#{match_id}")
end
JSON.parse(data, symbolize_names: true) if data
else
# Timeout: remove from queue
cancel(user_id)
nil
end
end
def create_match(player1_id, player2_id)
match_id = SecureRandom.uuid
game_token = JwtService.encode(
match_id: match_id,
player1_id: player1_id,
player2_id: player2_id,
game_server: GAME_SERVER,
exp: 1.hour.from_now.to_i
)
match_data = {
match_id: match_id,
game_token: game_token,
game_server: GAME_SERVER,
players: [player1_id, player2_id]
}
# Store match data for both players
RedisConnection.with do |redis|
redis.set("#{MATCH_PREFIX}match:#{match_id}", match_data.to_json, ex: MATCH_TTL)
redis.set("#{MATCH_PREFIX}#{player1_id}", match_id, ex: MATCH_TTL)
redis.set("#{MATCH_PREFIX}#{player2_id}", match_id, ex: MATCH_TTL)
end
match_data
end
end
end
+7
View File
@@ -0,0 +1,7 @@
# Redis configuration
# Development/test: localhost:6379 (default)
# Production: set REDIS_URL env variable, e.g. redis://redis.example.com:6379/0
RedisConnection = ConnectionPool.new(size: 5, timeout: 5) do
Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6380/0"))
end
+3
View File
@@ -4,6 +4,9 @@ Rails.application.routes.draw do
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "me", to: "sessions#me"
post "matchmaking", to: "matchmaking#create"
delete "matchmaking", to: "matchmaking#destroy"
end
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.