123 lines
3.3 KiB
Ruby
123 lines
3.3 KiB
Ruby
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: 2.minute.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
|