ruby-on-railszeromqquake

ZeroMQ Subscriber in Rails Worker Fails to Receive Messages, but Works with QLStats


I am trying to subscribe to the ZeroMQ from my Quake Live server. I registered the server on QLStats (green status here: https://qlstats.net/panel2/servers.html), confirming that the PUB socket is functioning correctly. Below I pasted log entries where I can see the QLStats server authenticating correctly. However, my Rails worker using ffi-rzmq fails to receive any messages (I am running this locally on a brand new project).

Here is the Rails worker code:

require 'ffi-rzmq'
require 'json'

class ZmqSubscriberJob
  include Sidekiq::Job

  def perform
    context = ZMQ::Context.new
    subscriber = context.socket(ZMQ::SUB)

    username = ENV['ZMQ_USERNAME']&.b
    password = ENV['ZMQ_PASSWORD']&.b
    subscriber.setsockopt(ZMQ::PLAIN_USERNAME, username)
    subscriber.setsockopt(ZMQ::PLAIN_PASSWORD, password)

    zmq_url = "tcp://#{ENV['ZMQ_HOST']}:#{ENV['ZMQ_PORT']}"
    subscriber.connect(zmq_url)
    subscriber.setsockopt(ZMQ::SUBSCRIBE, '')

    Rails.logger.info "Connected to ZeroMQ PUB socket at: #{zmq_url}, listening for all events."

    loop do
      message = ''
      subscriber.recv_string(message)
      Rails.logger.info "Received message: #{message}"
    rescue => e
      Rails.logger.error "Error: #{e.message}"
    end
  ensure
    subscriber&.close
    context&.terminate
  end
end

What I Have Verified:

  1. The Quake Live server logs confirm the PUB socket is running:
zmq PUB socket: tcp://0.0.0.0:27960
  1. I can also see that the QLStats server is logging in correctly:
I: 25-01-02 10:39:09 zauth: ZAP request mechanism=PLAIN ipaddress=23.88.103.193
I: 25-01-02 10:39:09 zauth: - allowed (PLAIN) username=XXX password=XXX
I: 25-01-02 10:39:09 zauth: - ZAP reply status_code=200 status_text=OK
  1. ZMQ_USERNAME and ZMQ_PASSWORD match the credentials configured in the server and shown in the logs.
  2. No error is logged by the Rails worker, but it does not receive any messages.

Environment Details

I looked at this to get a grasp about how it works: https://github.com/MinoMino/minqlx/blob/master/python/minqlx/_zmq.py

I tried to create a Python version just in case, but that didn't work either. I also created one in plain rails (no workers), no luck.

I believe it's something really simple that I am missing.

Could it be a problem with the version of ffi-rzmq? Any pointers on how to solve this?


Solution

  • And of course after 2 days banging my head, the minute after I posted this I found the solution. I needed to set the ZAP_DOMAIN too. I believe this is also why in the quake live server logs, for the username, you see stats_stats, but it's actually concatenating the domain with the username with _. So the username is stats, and the domain is stats. Below a working version of a simple ruby listener:

    require 'ffi-rzmq'
    require 'json'
    
    # Define constants for ZMQ connection
    ZMQ_USERNAME = 'stats'.b # this is the default username, AFAIK you cannot change it
    ZMQ_PASSWORD = 'XXXX'.b
    ZMQ_HOST = < add your server IP >
    ZMQ_PORT = '27960' # change to the right port in case
    ZMQ_URL = "tcp://#{ZMQ_HOST}:#{ZMQ_PORT}"
    ZAP_DOMAIN = 'stats'.b  # Explicit ZAP domain
    
    begin
      # Create ZMQ context and subscriber socket
      context = ZMQ::Context.new
      subscriber = context.socket(ZMQ::SUB)
    
      # Set authentication credentials
      if ZMQ_USERNAME && ZMQ_PASSWORD
        subscriber.setsockopt(ZMQ::PLAIN_USERNAME, ZMQ_USERNAME)
        subscriber.setsockopt(ZMQ::PLAIN_PASSWORD, ZMQ_PASSWORD)
        puts "Authentication credentials set. Username: #{ZMQ_USERNAME}, Password: #{ZMQ_PASSWORD}"
      else
        puts "No authentication credentials provided."
      end
    
      # Set the ZAP domain
      subscriber.setsockopt(ZMQ::ZAP_DOMAIN, ZAP_DOMAIN)
      puts "ZAP domain set to: #{ZAP_DOMAIN}"
    
      # Connect to ZMQ server
      subscriber.connect(ZMQ_URL)
      puts "Connecting to ZeroMQ PUB socket at: #{ZMQ_URL}"
    
      # Subscribe to all messages
      subscriber.setsockopt(ZMQ::SUBSCRIBE, '')
      puts "Subscribed to ZeroMQ PUB socket, listening for all events."
    
      # Listen for messages
      loop do
        message = ''
        begin
          subscriber.recv_string(message)
          puts "Received message: #{message}"
        rescue ZMQ::Error => e
          puts "ZeroMQ error: #{e.message}"
          break
        rescue => e
          puts "Error receiving message: #{e.message}"
          break
        end
      end
    rescue => e
      puts "Fatal error: #{e.message}"
    ensure
      subscriber&.close
      context&.terminate
      puts "ZeroMQ subscriber stopped."
    end