Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,23 @@ runs:
echo "::endgroup::"

echo "::group::Starting Redis servers"
redis_major_version=$(echo "$REDIS_VERSION" | grep -oP '^\d+')
set -x
# Check if REDIS_VERSION is in the custom map
mapped_version=""
if [[ -n "${REDIS_VERSION_CUSTOM_MAP:-}" ]]; then
for mapping in $REDIS_VERSION_CUSTOM_MAP; do
tag="${mapping%%:*}"
version="${mapping##*:}"
if [[ "$REDIS_VERSION" == "$tag" ]]; then
mapped_version="$version"
echo "Found custom mapping: $REDIS_VERSION -> $mapped_version"
break
fi
done
fi
# Use mapped version if found, otherwise use REDIS_VERSION
version_to_parse="${mapped_version:-$REDIS_VERSION}"
redis_major_version=$(echo "$version_to_parse" | grep -oP '^\d+')
echo "REDIS_MAJOR_VERSION=${redis_major_version}" >> $GITHUB_ENV

if (( redis_major_version < 8 )); then
Expand Down Expand Up @@ -117,6 +133,7 @@ runs:
fi

sleep 10 # time to settle
set +x
echo "::endgroup::"
shell: bash

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ env:
# for example after 8.2.1 is published, 8.2 image contains 8.2.1 content
CURRENT_CLIENT_LIBS_TEST_STACK_IMAGE_TAG: '8.4.0'
CURRENT_REDIS_VERSION: '8.4.0'
REDIS_VERSION_CUSTOM_MAP: 'custom-21651605017-debian-amd64:8.6'

jobs:
dependency-audit:
Expand Down Expand Up @@ -76,7 +77,7 @@ jobs:
max-parallel: 15
fail-fast: false
matrix:
redis-version: ['8.6-rc1-21356658603-debian-amd64', '${{ needs.redis_version.outputs.CURRENT }}', '8.2', '8.0.2' ,'7.4.4', '7.2.9']
redis-version: ['custom-21651605017-debian-amd64', '${{ needs.redis_version.outputs.CURRENT }}', '8.2', '8.0.2' ,'7.4.4', '7.2.9']
python-version: ['3.10', '3.14']
parser-backend: ['plain']
event-loop: ['asyncio']
Expand Down
1 change: 1 addition & 0 deletions redis/_parsers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,7 @@ def string_keys_to_dict(key_string, callback):
map(lambda ll: (float(ll[0]), float(ll[1])) if ll is not None else None, r)
),
"HGETALL": lambda r: r and pairs_to_dict(r) or {},
"HOTKEYS GET": lambda r: [pairs_to_dict(m) for m in r],
"MEMORY STATS": parse_memory_stats,
"MODULE LIST": lambda r: [pairs_to_dict(m) for m in r],
"RESET": str_if_bytes,
Expand Down
105 changes: 105 additions & 0 deletions redis/commands/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
AsyncScriptCommands,
DataAccessCommands,
FunctionCommands,
HotkeysMetricsTypes,
ManagementCommands,
ModuleCommands,
PubSubCommands,
Expand Down Expand Up @@ -827,6 +828,58 @@ def client_tracking_off(
target_nodes=target_nodes,
)

def hotkeys_start(
self,
metrics: List[HotkeysMetricsTypes],
count: Optional[int] = None,
duration: Optional[int] = None,
sample_ratio: Optional[int] = None,
slots: Optional[List[int]] = None,
**kwargs,
) -> ResponseT:
"""
Start collecting hotkeys data on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-start
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

def hotkeys_stop(self, **kwargs) -> ResponseT:
"""
Stop the ongoing hotkeys collection session (if any) on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-stop
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

def hotkeys_reset(self, **kwargs) -> ResponseT:
"""
Discard the last hotkeys collection session results on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-reset
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

def hotkeys_get(self, **kwargs) -> ResponseT:
"""
Retrieve the result of the collection session from the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-get
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)


class AsyncClusterManagementCommands(
ClusterManagementCommands, AsyncManagementCommands
Expand Down Expand Up @@ -924,6 +977,58 @@ async def client_tracking_off(
target_nodes=target_nodes,
)

async def hotkeys_start(
self,
metrics: List[HotkeysMetricsTypes],
count: Optional[int] = None,
duration: Optional[int] = None,
sample_ratio: Optional[int] = None,
slots: Optional[List[int]] = None,
**kwargs,
) -> ResponseT:
"""
Start collecting hotkeys data on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-start
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

async def hotkeys_stop(self, **kwargs) -> ResponseT:
"""
Stop the ongoing hotkeys collection session (if any) on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-stop
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

async def hotkeys_reset(self, **kwargs) -> ResponseT:
"""
Discard the last hotkeys collection session results on the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-reset
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)

async def hotkeys_get(self, **kwargs) -> ResponseT:
"""
Retrieve the result of the collection session from the specified node(s).
The command will be sent to the specified target_nodes.

For more information see https://redis.io/commands/hotkeys-get
"""
raise NotImplementedError(
"HOTKEYS commands are not supported in cluster mode. Please use the non-cluster client."
)


class ClusterDataAccessCommands(DataAccessCommands):
"""
Expand Down
84 changes: 84 additions & 0 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ def acl_whoami(self, **kwargs) -> ResponseT:
AsyncACLCommands = ACLCommands


class HotkeysMetricsTypes(Enum):
CPU = "CPU"
NET = "NET"


class ManagementCommands(CommandsProtocol):
"""
Redis management commands
Expand Down Expand Up @@ -1406,6 +1411,85 @@ def failover(self):
"FAILOVER is intentionally not implemented in the client."
)

def hotkeys_start(
self,
metrics: List[HotkeysMetricsTypes],
count: Optional[int] = None,
duration: Optional[int] = None,
sample_ratio: Optional[int] = None,
slots: Optional[List[int]] = None,
**kwargs,
) -> ResponseT:
"""
Start collecting hotkeys data.
Returns an error if there is an ongoing collection session.

Args:
count: The number of keys to collect in each criteria (CPU and network consumption)
metrics: List of metrics to track. Supported values: [HotkeysMetricsTypes.CPU, HotkeysMetricsTypes.NET]
duration: Automatically stop the collection after `duration` seconds
sample_ratio: Commands are sampled with probability 1/ratio (1 means no sampling)
slots: Only track keys on the specified hash slots

For more information, see https://redis.io/commands/hotkeys-start
"""
args: List[Union[str, int]] = ["HOTKEYS", "START"]

# Add METRICS
args.extend(["METRICS", len(metrics)])
args.extend([str(m.value) for m in metrics])

# Add COUNT
if count is not None:
args.extend(["COUNT", count])

# Add optional DURATION
if duration is not None:
args.extend(["DURATION", duration])

# Add optional SAMPLE ratio
if sample_ratio is not None:
args.extend(["SAMPLE", sample_ratio])

# Add optional SLOTS
if slots is not None:
args.append("SLOTS")
args.append(len(slots))
args.extend(slots)

return self.execute_command(*args, **kwargs)

def hotkeys_stop(self, **kwargs) -> ResponseT:
"""
Stop the ongoing hotkeys collection session (if any).
The results of the last collection session are kept for consumption with HOTKEYS GET.

For more information, see https://redis.io/commands/hotkeys-stop
"""
return self.execute_command("HOTKEYS STOP", **kwargs)

def hotkeys_reset(self, **kwargs) -> ResponseT:
"""
Discard the last hotkeys collection session results (in order to save memory).
Error if there is an ongoing collection session.

For more information, see https://redis.io/commands/hotkeys-reset
"""
return self.execute_command("HOTKEYS RESET", **kwargs)

def hotkeys_get(self, **kwargs) -> ResponseT:
"""
Retrieve the result of the ongoing collection session (if any),
or the last collection session (if any).

HOTKEYS GET response is wrapped in an array for aggregation support.
Each node returns a single-element array, allowing multiple node
responses to be concatenated by DMC or other aggregators.

For more information, see https://redis.io/commands/hotkeys-get
"""
return self.execute_command("HOTKEYS GET", **kwargs)


class AsyncManagementCommands(ManagementCommands):
async def command_info(self, **kwargs) -> None:
Expand Down
14 changes: 14 additions & 0 deletions tests/test_asyncio/test_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
LoadBalancingStrategy,
get_node_name,
)
from redis.commands.core import HotkeysMetricsTypes
from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot
from redis.exceptions import (
AskError,
Expand Down Expand Up @@ -2465,6 +2466,19 @@ async def test_acl_log(

await user_client.aclose()

@skip_if_server_version_lt("8.5.240")
async def test_hotkeys_cluster(self, r: RedisCluster) -> None:
"""Test all HOTKEYS commands in cluster are raising an error"""

with pytest.raises(NotImplementedError):
await r.hotkeys_start(count=10, metrics=[HotkeysMetricsTypes.CPU])
with pytest.raises(NotImplementedError):
await r.hotkeys_get()
with pytest.raises(NotImplementedError):
await r.hotkeys_reset()
with pytest.raises(NotImplementedError):
await r.hotkeys_stop()


class TestNodesManager:
"""
Expand Down
Loading
Loading