blob: 2b4423cb8aa4159ad397c1f3c32236ad93b175be [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import datetime
import pytest
from apache_iggy import CacheMetricsKey, GlobalPermissions, IggyClient, Permissions
from apache_iggy import SendMessage as Message
from .utils import (
get_server_config,
login_fresh_client,
unique_credentials,
wait_for_server,
)
# Fields that describe the server process itself and must not change between
# two calls within one server run.
PROCESS_IDENTITY_FIELDS = (
"process_id",
"start_time",
"hostname",
"os_name",
"os_version",
"kernel_version",
"iggy_server_version",
"iggy_server_semver",
)
class TestStats:
"""Test server statistics retrieval."""
@pytest.mark.asyncio
async def test_get_stats(self, iggy_client: IggyClient, unique_name):
"""Sending messages moves the server counts reported by get_stats."""
stats_before = await iggy_client.get_stats()
stream_name = unique_name()
topic_name = unique_name()
await iggy_client.create_stream(stream_name)
await iggy_client.create_topic(
stream=stream_name, name=topic_name, partitions_count=1
)
await iggy_client.send_messages(
stream=stream_name,
topic=topic_name,
partitioning=0,
messages=[Message(f"stats message {i}") for i in range(3)],
)
stats = await iggy_client.get_stats()
# `>=` rather than exact equality: the counters are server-global, so
# concurrently running tests (e.g. under pytest-xdist) may bump them too.
assert stats.streams_count >= stats_before.streams_count + 1
assert stats.topics_count >= stats_before.topics_count + 1
assert stats.partitions_count >= stats_before.partitions_count + 1
assert stats.messages_count >= stats_before.messages_count + 3
assert stats.messages_size_bytes > stats_before.messages_size_bytes
assert stats.clients_count >= 1
assert stats.iggy_server_version
assert stats.hostname
assert stats.os_name
assert stats.os_version
assert stats.kernel_version
assert stats.process_id > 0
# sysinfo cannot enumerate a process's threads on macOS, so a server
# running there reports 0.
if stats.os_name != "Darwin":
assert stats.threads_count > 0
assert stats.start_time > 0
assert stats.total_memory > 0
assert stats.available_memory <= stats.total_memory
assert stats.total_disk_space > 0
assert stats.free_disk_space <= stats.total_disk_space
assert isinstance(stats.run_time, datetime.timedelta)
assert stats.run_time >= stats_before.run_time
for field in PROCESS_IDENTITY_FIELDS:
assert getattr(stats, field) == getattr(stats_before, field)
assert f"streams_count={stats.streams_count}" in repr(stats)
assert stats.hostname in repr(stats)
@pytest.mark.asyncio
async def test_get_stats_reflects_topology(
self, iggy_client: IggyClient, unique_name
):
"""Streams, topics, partitions, consumer groups and a second client show
up in the counters, and deleting the streams brings them back down."""
stats_before = await iggy_client.get_stats()
streams = [unique_name() for _ in range(2)]
topics_per_stream = 2
partitions_per_topic = 3
topics = []
for stream_name in streams:
await iggy_client.create_stream(stream_name)
for _ in range(topics_per_stream):
topic_name = unique_name()
await iggy_client.create_topic(
stream=stream_name,
name=topic_name,
partitions_count=partitions_per_topic,
)
await iggy_client.create_consumer_group(
stream_name, topic_name, unique_name()
)
topics.append((stream_name, topic_name))
# Bound only to keep a second connection open until the test ends.
_second_client = await login_fresh_client("iggy", "iggy")
topics_created = len(topics)
partitions_created = topics_created * partitions_per_topic
stats = await iggy_client.get_stats()
assert stats.streams_count >= stats_before.streams_count + len(streams)
assert stats.topics_count >= stats_before.topics_count + topics_created
assert (
stats.partitions_count >= stats_before.partitions_count + partitions_created
)
# Every new partition opens with one segment.
assert stats.segments_count >= stats_before.segments_count + partitions_created
assert (
stats.consumer_groups_count
>= stats_before.consumer_groups_count + topics_created
)
# Both `iggy_client` and `_second_client` are connected at this point, so
# the cross-shard total covers them regardless of what else the server
# reaped in between.
assert stats.clients_count >= 2
# The SDK has no delete_stream, so the streams stay behind and only the
# topic-level counters are expected to drop. The baseline is re-read
# right before the deletes to keep the window in which a concurrent
# test can bump the server-global counters as small as possible.
stats_before_delete = await iggy_client.get_stats()
for stream_name, topic_name in topics:
await iggy_client.delete_topic(stream_name, topic_name)
stats_after = await iggy_client.get_stats()
assert (
stats_after.topics_count
<= stats_before_delete.topics_count - topics_created
)
assert (
stats_after.partitions_count
<= stats_before_delete.partitions_count - partitions_created
)
assert (
stats_after.segments_count
<= stats_before_delete.segments_count - partitions_created
)
assert (
stats_after.consumer_groups_count
<= stats_before_delete.consumer_groups_count - topics_created
)
@pytest.mark.asyncio
async def test_get_stats_cache_metrics_dict(self, iggy_client: IggyClient):
"""cache_metrics is a dict the server currently leaves empty."""
stats = await iggy_client.get_stats()
assert stats.cache_metrics == {}
@pytest.mark.unit
def test_cache_metrics_key_is_constructible_and_hashable(self):
"""A key built in Python can address a cache_metrics dict entry."""
key = CacheMetricsKey(stream_id=1, topic_id=2, partition_id=3)
assert key.stream_id == 1
assert key.topic_id == 2
assert key.partition_id == 3
assert repr(key) == "CacheMetricsKey(stream_id=1, topic_id=2, partition_id=3)"
equal_key = CacheMetricsKey(stream_id=1, topic_id=2, partition_id=3)
other_key = CacheMetricsKey(stream_id=1, topic_id=2, partition_id=4)
assert key == equal_key
assert key != other_key
assert hash(key) == hash(equal_key)
# An equal key constructed independently hits the same dict slot.
metrics_by_key = {key: "metrics"}
assert metrics_by_key[equal_key] == "metrics"
assert other_key not in metrics_by_key
@pytest.mark.asyncio
async def test_get_stats_requires_connection_and_auth(self):
"""get_stats fails before connecting, before login, and after logout."""
host, port = get_server_config()
wait_for_server(host, port)
client = IggyClient(f"{host}:{port}")
with pytest.raises(RuntimeError, match="Not connected"):
await client.get_stats()
await client.connect()
with pytest.raises(RuntimeError, match="Unauthenticated"):
await client.get_stats()
await client.login_user("iggy", "iggy")
await client.get_stats()
await client.logout_user()
with pytest.raises(RuntimeError, match="Unauthenticated"):
await client.get_stats()
@pytest.mark.asyncio
@pytest.mark.parametrize("flag", ["read_servers", "manage_servers"])
async def test_get_stats_requires_server_permission(
self, iggy_client: IggyClient, unique_name, flag
):
"""A user without read_servers or manage_servers is denied; either grants."""
username, password = unique_credentials(unique_name)
created = await iggy_client.create_user(username, password)
try:
denied_client = await login_fresh_client(username, password)
with pytest.raises(RuntimeError, match="Unauthorized"):
await denied_client.get_stats()
await iggy_client.update_permissions(
created.id,
Permissions(global_permissions=GlobalPermissions(**{flag: True})),
)
granted_client = await login_fresh_client(username, password)
stats = await granted_client.get_stats()
assert stats.process_id > 0
finally:
await iggy_client.delete_user(created.id)