{"record":{"id":"471d16fc9519f7e8","repo":"redis/redis-py","slug":"len-uncovered-shard-channel-s-left-unreconcile","errorCode":null,"errorMessage":"{len(uncovered)} shard channel(s) left unreconciled; slot(s) not covered by the cluster: {uncovered!r}","messagePattern":"(.+?) shard channel\\(s\\) left unreconciled; slot\\(s\\) not covered by the cluster: (.+?)","errorType":"exception","errorClass":"SlotNotCoveredError","httpStatus":null,"severity":"warning","filePath":"redis/cluster.py","lineNumber":3338,"sourceCode":"                        e,\n                    )\n                    if first_migrate_error is None:\n                        first_migrate_error = e\n                    continue\n            # Garbage-collect per-node pubsubs that no longer hold any\n            # subscription so their connections are released.\n            for name, pubsub in list(self.node_pubsub_mapping.items()):\n                if not pubsub.subscribed:\n                    try:\n                        pubsub.reset()\n                    except Exception:\n                        pass\n                    self.node_pubsub_mapping.pop(name, None)\n        if uncovered:\n            # Surface the uncovered channels so the caller (and observer\n            # notification path) knows reconciliation was incomplete. All\n            # coverable siblings have already been migrated above.\n            raise SlotNotCoveredError(\n                f\"{len(uncovered)} shard channel(s) left unreconciled; \"\n                f\"slot(s) not covered by the cluster: {uncovered!r}\"\n            )\n        if first_migrate_error is not None and not made_progress:\n            # Every migration attempted in this pass failed transiently and\n            # nothing else made progress. Re-raise the first caught error\n            # (typically the root cause; later failures are often downstream\n            # symptoms of the same unreachable node) so the worker's done-\n            # callback surfaces a single representative failure through the\n            # same logger channel used for SlotNotCoveredError. Per-channel\n            # WARNINGs above preserve the full forensic detail.\n            raise first_migrate_error\n\n    def _migrate_shard_channel(self, channel, handler, old_name, new_node):\n        # Detach from the old per-node pubsub, best-effort: the old node may\n        # already be unreachable during migration / failover.\n        if old_name and old_name in self.node_pubsub_mapping:\n            old_pubsub = self.node_pubsub_mapping[old_name]","sourceCodeStart":3320,"sourceCodeEnd":3356,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/cluster.py#L3320-L3356","documentation":"Raised as a SlotNotCoveredError during shard-channel (SSUBSCRIBE) reconciliation when one or more shard channels could not be migrated to their new owning node because get_node_from_key() raised SlotNotCoveredError for those channels' slots. The reconciliation loop (cluster.py:3287-3341) defers channels whose slots are transiently uncovered, continues reconciling siblings, and then surfaces the unresolved list so the caller/logs know reconciliation was incomplete. Retry happens on the next slots-cache change notification.","triggerScenarios":"Subscribing to shard channels via SSUBSCRIBE, then a cluster topology change (failover, CLUSTER SETSLOT) occurs that moves some channels' slots to nodes the client hasn't discovered yet. The reconciliation pass cannot find an owner for those slots.","commonSituations":"Cluster failover or resharding while shard-channel subscriptions are active, causing slot ownership to shift faster than topology refresh can track.","solutions":["Treat this as transient: the client retries reconciliation on the next topology change. Ensure topology refresh is enabled and reinitialize_steps > 0.","Verify the cluster eventually covers all relevant slots (CLUSTER NODES) and no slots are permanently unassigned.","If persistent, trigger a manual topology refresh (client.cluster_reload_slots()) and allow the next reconciliation pass to retry.","Log the uncovered channels from the error message to identify which subscriptions need attention."],"exampleFix":"null","handlingStrategy":"retry","validationCode":"# Before relying on shard subscriptions, verify slot coverage for channels\nfrom redis.cluster import key_slot\nfor channel in my_shard_channels:\n    slot = key_slot(channel.encode()) % 16384\n    if not client.nodes_manager.slots_cache.get(slot):\n        client.cluster_reload_slots()\n        break","typeGuard":"def shard_channels_covered(client, channels: list) -> bool:\n    from redis.cluster import key_slot\n    for ch in channels:\n        slot = key_slot(ch.encode() if isinstance(ch, str) else ch) % 16384\n        if not client.nodes_manager.slots_cache.get(slot):\n            return False\n    return True","tryCatchPattern":"from redis.exceptions import SlotNotCoveredError\nimport time\nfor attempt in range(3):\n    try:\n        # trigger reconciliation or the operation that surfaces it\n        break\n    except SlotNotCoveredError as e:\n        if 'unreconciled' in str(e):\n            client.cluster_reload_slots()\n            time.sleep(1)\n        else:\n            raise","preventionTips":["Treat shard-channel reconciliation errors as transient; the client retries on the next topology notification.","Ensure reinitialize_steps > 0 so topology refresh runs on slot errors.","Verify cluster health and full slot coverage when these errors persist."],"tags":["shard-channel","pubsub","slot-coverage","failover","reconciliation"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}