From 02b3ada6e030f8e06dd206c175e6349f18a53815 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Fri, 28 Aug 2026 10:38:18 +0100 Subject: [PATCH] DOC-7010 Add the xadd2 idempotent-XADD step for Ruby and ioredis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `xadd2` step (IDMP/IDMPAUTO dedup, XCFGSET) to the Ruby and ioredis cmds_stream examples, closing the last real Cause-B gap DOC-6968 left behind. commands/xadd.md regains full client tabs on xadd2. Neither client's harness-pinned version has typed support for this yet: redis-rb 6.0.0's `xadd` has no `idmp`/`idmpauto` keyword, and ioredis 5.11.1's bundled command table doesn't even list `xcfgset` (it's a brand-new Redis 8.6 command). Both steps use the raw `.call(...)` escape hatch for the whole XADD/XCFGSET call instead of a typed wrapper. Verified against Redis 8.8.0 in a throwaway Docker container, since the ambient local Redis was 7.2.7 — IDMP/XCFGSET don't exist there and would fail silently different (unknown command) rather than behave differently. Learned: redis-rb 6.0.0 and ioredis 5.11.1 have no typed IDMP/IDMPAUTO/XCFGSET support — both are call()'d raw instead of via a typed method Constraint: don't rewrite these two steps to use r.xadd(..., idmp: ...) or redis.xcfgset(...) — those methods/kwargs don't exist in the pinned client versions and would raise Gaps: if redis-rb or ioredis ship typed IDMP/XCFGSET support later, this can be revisited to use idiomatic calls instead of raw .call() Ticket: DOC-7010 --- .../cmds_stream/ioredis/cmds-stream.js | 26 +++++++++++++++++++ .../cmds_stream/ruby/cmds_stream.rb | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/local_examples/cmds_stream/ioredis/cmds-stream.js b/local_examples/cmds_stream/ioredis/cmds-stream.js index 7c6fd0a6e1..a49458f163 100644 --- a/local_examples/cmds_stream/ioredis/cmds-stream.js +++ b/local_examples/cmds_stream/ioredis/cmds-stream.js @@ -36,6 +36,32 @@ assert.deepEqual(res4[1][1], ['field1', 'value1', 'field2', 'value2', 'field3', await redis.del('mystream'); // REMOVE_END +// STEP_START xadd2 +const res5 = await redis.call('XADD', 'mystream', 'IDMP', 'producer1', 'msg1', '*', 'field', 'value'); +console.log(res5); // >>> 1726055713867-0 + +// Attempting to add the same message again with IDMP returns the original entry ID +const res6 = await redis.call('XADD', 'mystream', 'IDMP', 'producer1', 'msg1', '*', 'field', 'different_value'); +console.log(res6); // >>> 1726055713867-0 (same ID as res5, message was deduplicated) + +const res7 = await redis.call('XADD', 'mystream', 'IDMPAUTO', 'producer2', '*', 'field', 'value'); +console.log(res7); // >>> 1726055713867-1 + +// Auto-generated idempotent ID prevents duplicates for same producer+content +const res8 = await redis.call('XADD', 'mystream', 'IDMPAUTO', 'producer2', '*', 'field', 'value'); +console.log(res8); // >>> 1726055713867-1 (same ID as res7, duplicate detected) + +// Configure idempotent message processing settings +const res9 = await redis.call('XCFGSET', 'mystream', 'IDMP-DURATION', 300, 'IDMP-MAXSIZE', 1000); +console.log(res9); // >>> OK +// STEP_END + +// REMOVE_START +assert.equal(res5, res6); +assert.equal(res7, res8); +await redis.del('mystream'); +// REMOVE_END + // HIDE_START redis.disconnect(); // HIDE_END diff --git a/local_examples/cmds_stream/ruby/cmds_stream.rb b/local_examples/cmds_stream/ruby/cmds_stream.rb index 992839a09a..ef0dc8ad7d 100644 --- a/local_examples/cmds_stream/ruby/cmds_stream.rb +++ b/local_examples/cmds_stream/ruby/cmds_stream.rb @@ -38,5 +38,31 @@ def assert_equal(expected, actual) assert_equal({ 'name' => 'Sara', 'surname' => 'OConnor' }, res4[0][1]) assert_equal({ 'field1' => 'value1', 'field2' => 'value2', 'field3' => 'value3' }, res4[1][1]) r.del('mystream') +# REMOVE_END + +# STEP_START xadd2 +res5 = r.call('XADD', 'mystream', 'IDMP', 'producer1', 'msg1', '*', 'field', 'value') +puts res5 # >>> 1726055713867-0 + +# Attempting to add the same message again with IDMP returns the original entry ID +res6 = r.call('XADD', 'mystream', 'IDMP', 'producer1', 'msg1', '*', 'field', 'different_value') +puts res6 # >>> 1726055713867-0 (same ID as res5, message was deduplicated) + +res7 = r.call('XADD', 'mystream', 'IDMPAUTO', 'producer2', '*', 'field', 'value') +puts res7 # >>> 1726055713867-1 + +# Auto-generated idempotent ID prevents duplicates for same producer+content +res8 = r.call('XADD', 'mystream', 'IDMPAUTO', 'producer2', '*', 'field', 'value') +puts res8 # >>> 1726055713867-1 (same ID as res7, duplicate detected) + +# Configure idempotent message processing settings +res9 = r.call('XCFGSET', 'mystream', 'IDMP-DURATION', 300, 'IDMP-MAXSIZE', 1000) +puts res9 # >>> OK +# STEP_END + +# REMOVE_START +assert_equal(res5, res6) +assert_equal(res7, res8) +r.del('mystream') r.close # REMOVE_END