Skip to content

Commit b2d36d7

Browse files
Python: Add FUNCTION STATS command (#1794)
* Add cluster and standalone versions of FUNCTION STATS commands * Add transaction implementation for FUNCTION STATS * Start adding tests for FUNCTION STATS * Try finish implementing tests * Fix some issues in the tests * Fix tests * Document check_function_stats_response * Run linters and formatters * Import pytest * Fix import ordering * Fix mypy errors * Fix transaction tests * Address PR comments * Fix black lint * Address minor PR comments --------- Co-authored-by: aaron-congo <aaron.congo@improving.com>
1 parent c0e9e74 commit b2d36d7

File tree

8 files changed

+318
-3
lines changed

8 files changed

+318
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
* Node: Added LINDEX command ([#999](https://github.com/aws/glide-for-redis/pull/999))
209209
* Python, Node: Added ZPOPMAX command ([#996](https://github.com/aws/glide-for-redis/pull/996), [#1009](https://github.com/aws/glide-for-redis/pull/1009))
210210
* Python: Added DBSIZE command ([#1040](https://github.com/aws/glide-for-redis/pull/1040))
211+
* Python: Added FUNCTION STATS command ([#1794](https://github.com/aws/glide-for-redis/pull/1794))
211212

212213
#### Features
213214
* Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860))

python/python/glide/async_commands/cluster_commands.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
TClusterResponse,
1919
TEncodable,
2020
TFunctionListResponse,
21+
TFunctionStatsResponse,
2122
TResult,
2223
TSingleNodeRoute,
2324
)
@@ -555,6 +556,48 @@ async def fcall_ro_route(
555556
await self._execute_command(RequestType.FCallReadOnly, args, route),
556557
)
557558

559+
async def function_stats(
560+
self, route: Optional[Route] = None
561+
) -> TClusterResponse[TFunctionStatsResponse]:
562+
"""
563+
Returns information about the function that's currently running and information about the
564+
available execution engines.
565+
566+
See https://valkey.io/commands/function-stats/ for more details
567+
568+
Args:
569+
route (Optional[Route]): Specifies the routing configuration for the command. The client
570+
will route the command to the nodes defined by `route`.
571+
572+
Returns:
573+
TClusterResponse[TFunctionStatsResponse]: A `Mapping` with two keys:
574+
- `running_script` with information about the running script.
575+
- `engines` with information about available engines and their stats.
576+
See example for more details.
577+
578+
Examples:
579+
>>> await client.function_stats(RandomNode())
580+
{
581+
'running_script': {
582+
'name': 'foo',
583+
'command': ['FCALL', 'foo', '0', 'hello'],
584+
'duration_ms': 7758
585+
},
586+
'engines': {
587+
'LUA': {
588+
'libraries_count': 1,
589+
'functions_count': 1,
590+
}
591+
}
592+
}
593+
594+
Since: Redis version 7.0.0.
595+
"""
596+
return cast(
597+
TClusterResponse[TFunctionStatsResponse],
598+
await self._execute_command(RequestType.FunctionStats, [], route),
599+
)
600+
558601
async def function_dump(
559602
self, route: Optional[Route] = None
560603
) -> TClusterResponse[bytes]:

python/python/glide/async_commands/standalone_commands.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
_build_sort_args,
1414
)
1515
from glide.async_commands.transaction import BaseTransaction, Transaction
16-
from glide.constants import OK, TOK, TEncodable, TFunctionListResponse, TResult
16+
from glide.constants import (
17+
OK,
18+
TOK,
19+
TEncodable,
20+
TFunctionListResponse,
21+
TFunctionStatsResponse,
22+
TResult,
23+
)
1724
from glide.protobuf.redis_request_pb2 import RequestType
1825

1926

@@ -362,6 +369,42 @@ async def function_delete(self, library_name: TEncodable) -> TOK:
362369
),
363370
)
364371

372+
async def function_stats(self) -> TFunctionStatsResponse:
373+
"""
374+
Returns information about the function that's currently running and information about the
375+
available execution engines.
376+
377+
See https://valkey.io/commands/function-stats/ for more details
378+
379+
Returns:
380+
TFunctionStatsResponse: A `Mapping` with two keys:
381+
- `running_script` with information about the running script.
382+
- `engines` with information about available engines and their stats.
383+
See example for more details.
384+
385+
Examples:
386+
>>> await client.function_stats()
387+
{
388+
'running_script': {
389+
'name': 'foo',
390+
'command': ['FCALL', 'foo', '0', 'hello'],
391+
'duration_ms': 7758
392+
},
393+
'engines': {
394+
'LUA': {
395+
'libraries_count': 1,
396+
'functions_count': 1,
397+
}
398+
}
399+
}
400+
401+
Since: Redis version 7.0.0.
402+
"""
403+
return cast(
404+
TFunctionStatsResponse,
405+
await self._execute_command(RequestType.FunctionStats, []),
406+
)
407+
365408
async def function_dump(self) -> bytes:
366409
"""
367410
Returns the serialized payload of all loaded libraries.

python/python/glide/async_commands/transaction.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,23 @@ def fcall_ro(
20192019
args.extend(arguments)
20202020
return self.append_command(RequestType.FCallReadOnly, args)
20212021

2022+
def function_stats(self: TTransaction) -> TTransaction:
2023+
"""
2024+
Returns information about the function that's currently running and information about the
2025+
available execution engines.
2026+
2027+
See https://valkey.io/commands/function-stats/ for more details
2028+
2029+
Command Response:
2030+
TFunctionStatsResponse: A `Mapping` with two keys:
2031+
- `running_script` with information about the running script.
2032+
- `engines` with information about available engines and their stats.
2033+
See example for more details.
2034+
2035+
Since: Redis version 7.0.0.
2036+
"""
2037+
return self.append_command(RequestType.FunctionStats, [])
2038+
20222039
def xadd(
20232040
self: TTransaction,
20242041
key: TEncodable,

python/python/glide/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@
4242
Union[bytes, List[Mapping[bytes, Union[bytes, Set[bytes]]]]],
4343
]
4444
]
45+
TFunctionStatsResponse = Mapping[
46+
bytes,
47+
Union[
48+
None,
49+
Mapping[
50+
bytes, Union[Mapping[bytes, Mapping[bytes, int]], bytes, int, List[bytes]]
51+
],
52+
],
53+
]

python/python/tests/test_async_client.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
ProtocolVersion,
7777
ServerCredentials,
7878
)
79-
from glide.constants import OK, TEncodable, TResult
79+
from glide.constants import OK, TEncodable, TFunctionStatsResponse, TResult
8080
from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient
8181
from glide.routes import (
8282
AllNodes,
@@ -91,6 +91,7 @@
9191
from tests.conftest import create_client
9292
from tests.utils.utils import (
9393
check_function_list_response,
94+
check_function_stats_response,
9495
check_if_server_version_lt,
9596
compare_maps,
9697
convert_bytes_to_string_object,
@@ -8087,6 +8088,153 @@ async def test_function_delete_with_routing(
80878088
await glide_client.function_delete(lib_name)
80888089
assert "Library not found" in str(e)
80898090

8091+
@pytest.mark.parametrize("cluster_mode", [False])
8092+
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
8093+
async def test_function_stats(self, glide_client: GlideClient):
8094+
min_version = "7.0.0"
8095+
if await check_if_server_version_lt(glide_client, min_version):
8096+
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
8097+
8098+
lib_name = "functionStats"
8099+
func_name = lib_name
8100+
assert await glide_client.function_flush(FlushMode.SYNC) == OK
8101+
8102+
# function $funcName returns first argument
8103+
code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False)
8104+
assert await glide_client.function_load(code, True) == lib_name.encode()
8105+
8106+
response = await glide_client.function_stats()
8107+
check_function_stats_response(response, [], 1, 1)
8108+
8109+
code = generate_lua_lib_code(
8110+
lib_name + "_2",
8111+
{func_name + "_2": "return 'OK'", func_name + "_3": "return 42"},
8112+
False,
8113+
)
8114+
assert (
8115+
await glide_client.function_load(code, True) == (lib_name + "_2").encode()
8116+
)
8117+
8118+
response = await glide_client.function_stats()
8119+
check_function_stats_response(response, [], 2, 3)
8120+
8121+
assert await glide_client.function_flush(FlushMode.SYNC) == OK
8122+
8123+
response = await glide_client.function_stats()
8124+
check_function_stats_response(response, [], 0, 0)
8125+
8126+
@pytest.mark.parametrize("cluster_mode", [True])
8127+
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
8128+
async def test_function_stats_cluster(self, glide_client: GlideClusterClient):
8129+
min_version = "7.0.0"
8130+
if await check_if_server_version_lt(glide_client, min_version):
8131+
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
8132+
8133+
lib_name = "functionStats_without_route"
8134+
func_name = lib_name
8135+
assert await glide_client.function_flush(FlushMode.SYNC) == OK
8136+
8137+
# function $funcName returns first argument
8138+
code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False)
8139+
assert await glide_client.function_load(code, True) == lib_name.encode()
8140+
8141+
response = await glide_client.function_stats()
8142+
for node_response in response.values():
8143+
check_function_stats_response(
8144+
cast(TFunctionStatsResponse, node_response), [], 1, 1
8145+
)
8146+
8147+
code = generate_lua_lib_code(
8148+
lib_name + "_2",
8149+
{func_name + "_2": "return 'OK'", func_name + "_3": "return 42"},
8150+
False,
8151+
)
8152+
assert (
8153+
await glide_client.function_load(code, True) == (lib_name + "_2").encode()
8154+
)
8155+
8156+
response = await glide_client.function_stats()
8157+
for node_response in response.values():
8158+
check_function_stats_response(
8159+
cast(TFunctionStatsResponse, node_response), [], 2, 3
8160+
)
8161+
8162+
assert await glide_client.function_flush(FlushMode.SYNC) == OK
8163+
8164+
response = await glide_client.function_stats()
8165+
for node_response in response.values():
8166+
check_function_stats_response(
8167+
cast(TFunctionStatsResponse, node_response), [], 0, 0
8168+
)
8169+
8170+
@pytest.mark.parametrize("cluster_mode", [True])
8171+
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
8172+
@pytest.mark.parametrize("single_route", [True, False])
8173+
async def test_function_stats_with_routing(
8174+
self, glide_client: GlideClusterClient, single_route: bool
8175+
):
8176+
min_version = "7.0.0"
8177+
if await check_if_server_version_lt(glide_client, min_version):
8178+
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")
8179+
8180+
route = (
8181+
SlotKeyRoute(SlotType.PRIMARY, get_random_string(10))
8182+
if single_route
8183+
else AllPrimaries()
8184+
)
8185+
lib_name = "functionStats_with_route_" + str(single_route)
8186+
func_name = lib_name
8187+
assert await glide_client.function_flush(FlushMode.SYNC, route) == OK
8188+
8189+
# function $funcName returns first argument
8190+
code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False)
8191+
assert await glide_client.function_load(code, True, route) == lib_name.encode()
8192+
8193+
response = await glide_client.function_stats(route)
8194+
if single_route:
8195+
check_function_stats_response(
8196+
cast(TFunctionStatsResponse, response), [], 1, 1
8197+
)
8198+
else:
8199+
for node_response in response.values():
8200+
check_function_stats_response(
8201+
cast(TFunctionStatsResponse, node_response), [], 1, 1
8202+
)
8203+
8204+
code = generate_lua_lib_code(
8205+
lib_name + "_2",
8206+
{func_name + "_2": "return 'OK'", func_name + "_3": "return 42"},
8207+
False,
8208+
)
8209+
assert (
8210+
await glide_client.function_load(code, True, route)
8211+
== (lib_name + "_2").encode()
8212+
)
8213+
8214+
response = await glide_client.function_stats(route)
8215+
if single_route:
8216+
check_function_stats_response(
8217+
cast(TFunctionStatsResponse, response), [], 2, 3
8218+
)
8219+
else:
8220+
for node_response in response.values():
8221+
check_function_stats_response(
8222+
cast(TFunctionStatsResponse, node_response), [], 2, 3
8223+
)
8224+
8225+
assert await glide_client.function_flush(FlushMode.SYNC, route) == OK
8226+
8227+
response = await glide_client.function_stats(route)
8228+
if single_route:
8229+
check_function_stats_response(
8230+
cast(TFunctionStatsResponse, response), [], 0, 0
8231+
)
8232+
else:
8233+
for node_response in response.values():
8234+
check_function_stats_response(
8235+
cast(TFunctionStatsResponse, node_response), [], 0, 0
8236+
)
8237+
80908238
@pytest.mark.parametrize("cluster_mode", [True])
80918239
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
80928240
async def test_fcall_with_key(self, glide_client: GlideClusterClient):

python/python/tests/test_transaction.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ async def transaction_test(
159159
args.append(OK)
160160
transaction.function_flush(FlushMode.SYNC)
161161
args.append(OK)
162+
transaction.function_stats()
163+
args.append(
164+
{
165+
b"running_script": None,
166+
b"engines": {
167+
b"LUA": {
168+
b"libraries_count": 0,
169+
b"functions_count": 0,
170+
}
171+
},
172+
}
173+
)
162174

163175
transaction.dbsize()
164176
args.append(0)

0 commit comments

Comments
 (0)