{"record":{"id":"8acd84f2faa3cdf9","repo":"redis/redis-rb","slug":"mapped-msetnx-cannot-be-used-in-redis-distributed","errorCode":null,"errorMessage":"MAPPED_MSETNX cannot be used in Redis::Distributed because the keys involved need to be on the same server or because we cannot guarantee that the operation will be atomic.","messagePattern":"MAPPED_MSETNX cannot be used in Redis::Distributed because the keys involved need to be on the same server or because we cannot guarantee that the operation will be atomic\\.","errorType":"exception","errorClass":"Redis::Distributed::CannotDistribute","httpStatus":null,"severity":"error","filePath":"lib/redis/distributed.rb","lineNumber":340,"sourceCode":"      node_for(key).setnx(key, value)\n    end\n\n    # Set multiple keys to multiple values.\n    def mset(*)\n      raise CannotDistribute, :mset\n    end\n\n    def mapped_mset(_hash)\n      raise CannotDistribute, :mapped_mset\n    end\n\n    # Set multiple keys to multiple values, only if none of the keys exist.\n    def msetnx(*)\n      raise CannotDistribute, :msetnx\n    end\n\n    def mapped_msetnx(_hash)\n      raise CannotDistribute, :mapped_msetnx\n    end\n\n    # Get the value of a key.\n    def get(key)\n      node_for(key).get(key)\n    end\n\n    # Get the value of a key and delete it.\n    def getdel(key)\n      node_for(key).getdel(key)\n    end\n\n    # Get the value of a key and sets its time to live based on options.\n    def getex(key, **options)\n      node_for(key).getex(key, **options)\n    end\n\n    # Set the JSON value at a path in the document stored under a key.","sourceCodeStart":322,"sourceCodeEnd":358,"githubUrl":"https://github.com/redis/redis-rb/blob/2ba9010b91dab9e0fde1fbae3a9aae003f8bc307/lib/redis/distributed.rb#L322-L358","documentation":"Redis::Distributed shards keys across N independent standalone Redis servers using a client-side consistent-hash ring (CRC32 over key or {tag}). MSETNX must set every key only if none of them exists, which the server can only guarantee when all keys live on one node. Because the ring may route the hash's keys to different nodes, the client raises CannotDistribute instead of silently breaking the all-or-nothing guarantee. Note that both msetnx and mapped_msetnx raise unconditionally — unlike tag-aware commands (e.g. geosearchstore) that use ensure_same_node, no key-tag arrangement makes them callable on the facade.","triggerScenarios":"Calling dist.mapped_msetnx({\"k1\" => \"a\", \"k2\" => \"b\"}) or dist.msetnx(\"k1\", \"a\", \"k2\", \"b\") on any Redis::Distributed instance, regardless of key tags. Typically code that ran against a standalone Redis client and is re-pointed at a Distributed client (Redis.new([...urls]) or Redis::Distributed.new), or shared library code that accepts either client class.","commonSituations":"Porting an app from one Redis server to a shard fleet; caching/session libraries (e.g. lock or claim-a-set-of-keys patterns such as idempotency markers) that call mapped_msetnx internally; test suites that exercise the same command layer against both Redis and Redis::Distributed clients; Sidekiq/Resque-style atomic multi-key claims.","solutions":["If cross-key atomicity is not required, replace with per-key conditional writes: hash.each { |k, v| dist.set(k, v, nx: true) } and check which keys returned true","If the keys can share a hash tag, route to the single node yourself: dist.node_for(\"{tag}k1\").mapped_msetnx(hash) — node_for honors the {tag} regex, and the underlying standalone client supports the command","Restructure to a single sentinel key (e.g. one hash or one lock key) so the atomic check fits one key","If the workload genuinely needs multi-key atomicity at scale, move to Redis::Cluster, where hash tags place keys in one server-side slot and MSETNX works"],"exampleFix":"# before\ndist.mapped_msetnx({ \"k1\" => \"a\", \"k2\" => \"b\" })\n# => Redis::Distributed::CannotDistribute\n\n# after (option 1: per-key, not atomic across keys)\nwritten = { \"k1\" => \"a\", \"k2\" => \"b\" }.each_with_object({}) do |(k, v), acc|\n  acc[k] = dist.set(k, v, nx: true)\nend\n\n# after (option 2: shared hash tag, atomic on one node)\ndist.node_for(\"{job1}k1\").mapped_msetnx({ \"{job1}k1\" => \"a\", \"{job1}k2\" => \"b\" })","handlingStrategy":"fallback","validationCode":"# Route multi-key conditional writes through an adapter before touching the client\ndef safe_mapped_msetnx(client, hash)\n  if client.is_a?(Redis::Distributed)\n    hash.each_with_object({}) { |(k, v), acc| acc[k] = client.set(k, v, nx: true) }\n  else\n    client.mapped_msetnx(hash)\n  end\nend","typeGuard":"def distributed_client?(client)\n  client.is_a?(Redis::Distributed)\nend","tryCatchPattern":"begin\n  dist.mapped_msetnx(hash)\nrescue Redis::Distributed::CannotDistribute => e\n  logger.warn(\"#{e.message}; falling back to per-key set nx\")\n  hash.each { |k, v| dist.set(k, v, nx: true) }\nend","preventionTips":["Treat Redis::Distributed as a strictly smaller API than Redis: audit for multi-key atomic commands (msetnx, mapped_msetnx, json_mset, and friends) before switching client classes","Keep multi-key atomic operations behind one adapter method so the fallback lives in exactly one place","Run your test suite against Redis::Distributed (rake test:distributed) before adopting it in production paths","Document which operations trade atomicity for shardability next to the adapter"],"tags":["redis","ruby","distributed","client-side-sharding","multi-key","atomicity","msetnx"],"backgroundTag":"redis-distributed-cannot-distribute","analyzedSha":"2ba9010b91dab9e0fde1fbae3a9aae003f8bc307","analyzedAt":"2026-08-23T03:54:57.017Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}