{"record":{"id":"8903a7afff82dec8","repo":"redis/redis-rb","slug":"unwatch-cannot-be-used-in-redis-distributed-becau","errorCode":null,"errorMessage":"UNWATCH 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":"UNWATCH 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":1317,"sourceCode":"    end\n\n    # Watch the given keys to determine execution of the MULTI/EXEC block.\n    def watch(*keys, &block)\n      ensure_same_node(:watch, keys) do |node|\n        @watch_key = key_tag(keys.first) || keys.first.to_s\n\n        begin\n          node.watch(*keys, &block)\n        rescue StandardError\n          @watch_key = nil\n          raise\n        end\n      end\n    end\n\n    # Forget about all watched keys.\n    def unwatch\n      raise CannotDistribute, :unwatch unless @watch_key\n\n      result = node_for(@watch_key).unwatch\n      @watch_key = nil\n      result\n    end\n\n    def pipelined\n      raise CannotDistribute, :pipelined\n    end\n\n    # Mark the start of a transaction block.\n    def multi(&block)\n      raise CannotDistribute, :multi unless @watch_key\n\n      node_for(@watch_key).multi(&block)\n    end\n\n    # Execute all commands issued after MULTI.","sourceCodeStart":1299,"sourceCodeEnd":1335,"githubUrl":"https://github.com/redis/redis-rb/blob/2ba9010b91dab9e0fde1fbae3a9aae003f8bc307/lib/redis/distributed.rb#L1299-L1335","documentation":"Redis::Distributed supports WATCH/MULTI only against a single node: #watch routes all watched keys (which must share a node via ensure_same_node) and records the watched key in @watch_key. #unwatch raises CannotDistribute when @watch_key is nil — that is, when no WATCH is in progress on this instance. The message about same-server/atomicity is misleading here; the real condition is missing transaction state. @watch_key is also cleared by #exec, #discard, and a failed #watch, so unwatch after any of those raises too.","triggerScenarios":"Calling dist.unwatch before any dist.watch on the instance; after #exec or #discard already consumed the watch; on a different Distributed instance than the one that called watch; mixing the block form of watch (which unwatches via the node) with a manual unwatch afterwards; a raised error inside a watch sequence leaving state out of sync.","commonSituations":"Porting standalone watch/unwatch choreography to a sharded client; cleanup code that unconditionally calls unwatch in an ensure block; multi-threaded code where one thread's exec clears the watch another thread tries to unwatch; generic transaction wrappers that mirror the Redis command sequence.","solutions":["Prefer the block form — dist.watch(\"k1\", \"k2\") { dist.multi { |txn| ... } } — the underlying node unwatches itself, including on failure","Only call unwatch when you know watch succeeded and neither exec nor discard has run since; keep that sequence inside one method so state cannot drift","If you must probe state first, check @watch_key: dist.unwatch if dist.instance_variable_get(:@watch_key)","In cleanup blocks, rescue Redis::Distributed::CannotDistribute and treat it as already-clean"],"exampleFix":"# before\ndist.watch(\"k1\")\nval = dist.get(\"k1\")\nif val\n  dist.multi { |txn| txn.set(\"k1\", new_val) }\nelse\n  dist.unwatch # fine here, but raises if exec already ran or watch never did\nend\n\n# after: block form — watch/unwatch lifecycle handled for you\ndist.watch(\"k1\") do\n  val = dist.get(\"k1\")\n  dist.multi { |txn| txn.set(\"k1\", new_val) } if val\nend","handlingStrategy":"validation","validationCode":"# The facade has no public watching? reader; check the same state it checks\ndist.unwatch if dist.instance_variable_get(:@watch_key)\n\n# Better: wrap the whole optimistic transaction so the state machine cannot be observed mid-flight\ndef optimistic_write(dist, key)\n  dist.watch(key) do\n    current = dist.get(key)\n    dist.multi { |txn| txn.set(key, yield(current)) }\n  end\nend","typeGuard":"def watching?(dist)\n  !dist.instance_variable_get(:@watch_key).nil?\nend","tryCatchPattern":"begin\n  dist.unwatch\nrescue Redis::Distributed::CannotDistribute\n  # no watch in progress — already clean\nend","preventionTips":["Prefer the block form of watch; it owns the unwatch, including on failure","Never split watch/multi/exec/unwatch across threads or methods you do not control","Remember that exec, discard, and a failed watch all clear the watched key — unwatch after any of them raises","Treat this CannotDistribute in cleanup paths as a no-op, not a distribution problem"],"tags":["redis","ruby","distributed","transactions","optimistic-locking","watch","state-error"],"backgroundTag":"redis-watch-transaction-state","analyzedSha":"2ba9010b91dab9e0fde1fbae3a9aae003f8bc307","analyzedAt":"2026-08-23T03:54:57.017Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}