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:
zmq PUB socket: tcp://0.0.0.0:27960
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
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?
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