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