From 1780145cf63f25f7d16b56c02de85b6bf509fdd4 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Fri, 30 Oct 2026 09:47:05 +0000 Subject: [PATCH] DOC-7226 Fix the Ruby job-queue and streaming demos on Ruby 4.0 and the index-readiness snippet The job-queue and streaming Ruby demos parsed form bodies with CGI.parse, which Ruby 4.0 removed, so every form POST returned 500. They now use URI.decode_www_form, the way the feature-store and recommendation-engine Ruby demos already do, and return a hash of arrays that reads a missing key as an empty array, as CGI.parse did. The streaming demo had a second bug that CGI hid: send_json takes keyword arguments (status:), and its success responses passed the payload as a braceless hash (send_json(res, 'produced' => n)), which Ruby 4.0 reads as keywords, so the call failed with a wrong-number-of-arguments error. Its error responses were already wrapped in braces; the seven success calls now are too. The check_index_readiness snippet on the index management page imported re instead of redis and ended its return at expected_d. Fixing those still left it broken on current redis-py, where execute_command returns FT.INFO as a dict under the new default protocol and info.index() raises, so it now reads r.ft(index_name).info()['num_docs'], which works on redis-py 5.2.1 and 8.1.0 under RESP2 and RESP3. Verification on Ruby 4.0.6 with no shim: the job-queue demo completed 8 of 8 jobs, and every streaming endpoint (produce, add and remove worker, state, replay, crash, autoclaim, trim, reset) returned 200. The page snippet was extracted from the published text and run against a real index on both redis-py versions. Learned: on Ruby 4.0 a braceless hash with string keys passed to a method that declares keyword arguments is read as keywords, so send_json(res, 'k' => v) fails with an arity error whenever the helper has status:, and wrapping the payload in braces fixes it (Ruby 2.6 accepts it; Ruby 3.x was not tested) Learned: redis-py 8 returns FT.INFO from execute_command as a dict, so code that indexes the raw reply by position breaks; use r.ft(name).info() Gaps: the Ruby port was tested on main without the BLMOVE change from the open job-queue PR, which merges cleanly with this one Ticket: DOC-7226 Co-Authored-By: Claude Sonnet 5.5 --- .../index-mgmt-best-practices.md | 10 +++-- .../use-cases/job-queue/ruby/demo_server.rb | 11 ++++- .../use-cases/streaming/ruby/demo_server.rb | 43 +++++++++++-------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/content/develop/ai/search-and-query/best-practices/index-mgmt-best-practices.md b/content/develop/ai/search-and-query/best-practices/index-mgmt-best-practices.md index 973d5c4821..c9f5436bb7 100644 --- a/content/develop/ai/search-and-query/best-practices/index-mgmt-best-practices.md +++ b/content/develop/ai/search-and-query/best-practices/index-mgmt-best-practices.md @@ -132,12 +132,14 @@ Monitoring and troubleshooting aliases: - Implement scripts to periodically verify document counts and query results. For example, in Python: ```python - import re + import redis + def check_index_readiness(index_name, expected_docs): r = redis.Redis(host='localhost', port=6379, decode_responses=True) - info = r.execute_command('FT.INFO', index_name) - num_docs = int(info[info.index('num_docs') + 1]) - return num_docs >= expected_d + info = r.ft(index_name).info() + num_docs = int(info['num_docs']) + return num_docs >= expected_docs + if check_index_readiness('my_new_index', 100000): print("Index is fully populated!") else: diff --git a/content/develop/use-cases/job-queue/ruby/demo_server.rb b/content/develop/use-cases/job-queue/ruby/demo_server.rb index 903aea34f1..ab662922e9 100644 --- a/content/develop/use-cases/job-queue/ruby/demo_server.rb +++ b/content/develop/use-cases/job-queue/ruby/demo_server.rb @@ -5,10 +5,10 @@ # pool of workers drain the queue, simulate worker crashes, and trigger a # reclaim sweep that pulls timed-out jobs back to pending. -require 'cgi' require 'json' require 'optparse' require 'redis' +require 'uri' require 'webrick' require_relative 'job_queue' @@ -37,8 +37,15 @@ def parse_args(argv) opts end +# Parse an application/x-www-form-urlencoded body into a Hash of value +# Arrays. A missing key reads as an empty Array, as CGI.parse did before it +# was dropped from Ruby's standard library in Ruby 4. def parse_form(body) - CGI.parse(body.to_s) + out = Hash.new { |h, k| h[k] = [] } + URI.decode_www_form(body.to_s).each { |k, v| out[k] << v } + out +rescue ArgumentError + Hash.new { |h, k| h[k] = [] } end def clamp_int(value, lo, hi, default) diff --git a/content/develop/use-cases/streaming/ruby/demo_server.rb b/content/develop/use-cases/streaming/ruby/demo_server.rb index e78fc5ae27..40adf96fec 100644 --- a/content/develop/use-cases/streaming/ruby/demo_server.rb +++ b/content/develop/use-cases/streaming/ruby/demo_server.rb @@ -18,11 +18,11 @@ # independent of consumer-group state. # * Trim the stream with XTRIM to bound retention. -require 'cgi' require 'json' require 'optparse' require 'redis' require 'thread' +require 'uri' require 'webrick' require_relative 'event_stream' @@ -185,8 +185,15 @@ def parse_args(argv) opts end +# Parse an application/x-www-form-urlencoded body into a Hash of value +# Arrays. A missing key reads as an empty Array, as CGI.parse did before it +# was dropped from Ruby's standard library in Ruby 4. def parse_form(body) - CGI.parse(body.to_s) + out = Hash.new { |h, k| h[k] = [] } + URI.decode_www_form(body.to_s).each { |k, v| out[k] << v } + out +rescue ArgumentError + Hash.new { |h, k| h[k] = [] } end def clamp_int(value, lo, hi, default) @@ -705,7 +712,7 @@ def do_POST(req, res) when '/trim' then handle_trim(req, res) when '/reset' count = @demo.reset - send_json(res, 'consumers' => count) + send_json(res, { 'consumers' => count }) else res.status = 404 res.body = 'not found' @@ -725,7 +732,7 @@ def handle_produce(req, res) [picked, fake_payload] end ids = @stream.produce_batch(events) - send_json(res, 'produced' => ids.length, 'ids' => ids) + send_json(res, { 'produced' => ids.length, 'ids' => ids }) end def handle_add_worker(req, res) @@ -740,7 +747,7 @@ def handle_add_worker(req, res) send_json(res, { 'error' => "#{group}/#{name} already exists" }, status: 409) return end - send_json(res, 'group' => group, 'name' => name) + send_json(res, { 'group' => group, 'name' => name }) end def handle_remove_worker(req, res) @@ -767,7 +774,7 @@ def handle_crash(req, res) return end worker.crash_next(count) - send_json(res, 'queued' => count) + send_json(res, { 'queued' => count }) end def handle_autoclaim(req, res) @@ -790,18 +797,19 @@ def handle_autoclaim(req, res) # in production they would be routed to a dead-letter store for # offline inspection. result = worker.reap_idle_pel - send_json(res, - 'claimed' => result[:claimed], - 'processed' => result[:processed], - 'deleted' => result[:deleted_ids], - 'min_idle_ms' => @stream.claim_min_idle_ms) + send_json(res, { + 'claimed' => result[:claimed], + 'processed' => result[:processed], + 'deleted' => result[:deleted_ids], + 'min_idle_ms' => @stream.claim_min_idle_ms + }) end def handle_trim(req, res) params = parse_form(req.body) maxlen = clamp_int((params['maxlen'] || ['0']).first || '0', 0, 1_000_000_000, 0) deleted = @stream.trim_maxlen(maxlen) - send_json(res, 'deleted' => deleted, 'maxlen' => maxlen) + send_json(res, { 'deleted' => deleted, 'maxlen' => maxlen }) end def handle_replay(req, res) @@ -810,11 +818,12 @@ def handle_replay(req, res) end_id = (query['end'].to_s.empty? ? '+' : query['end']) limit = clamp_int(query['count'].to_s.empty? ? '20' : query['count'], 1, 500, 20) entries = @stream.replay(start_id: start_id, end_id: end_id, count: limit) - send_json(res, - 'start' => start_id, - 'end' => end_id, - 'limit' => limit, - 'entries' => entries.map { |id, fields| { 'id' => id, 'fields' => fields } }) + send_json(res, { + 'start' => start_id, + 'end' => end_id, + 'limit' => limit, + 'entries' => entries.map { |id, fields| { 'id' => id, 'fields' => fields } } + }) end # ---- State assembly ---------------------------------------------