Skip to content

Commit 2d98ae3

Browse files
committed
fixup! Rearranged methods by importance, dropped debug display values
1 parent 8b2ce40 commit 2d98ae3

File tree

4 files changed

+108
-64
lines changed

4 files changed

+108
-64
lines changed

cratedb_toolkit/admin/xmover/analysis/shard.py

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"""
22
Shard analysis and rebalancing logic for CrateDB
33
"""
4-
import datetime
4+
5+
import enum
56
import logging
67
import math
78
from collections import defaultdict
9+
from datetime import datetime
810
from time import sleep
911
from typing import Any, Dict, List, Optional, Set, Tuple, Union
10-
from unittest import result
1112

1213
from rich import box
1314
from rich.console import Console
1415
from rich.panel import Panel
1516
from rich.table import Table
16-
from rich.live import Live
1717

1818
from cratedb_toolkit.admin.xmover.model import (
1919
DistributionStats,
@@ -839,93 +839,112 @@ def plan_node_decommission(self, node_name: str, min_free_space_gb: float = 100.
839839
}
840840

841841

842-
class ShardMonitor:
842+
class ShardHeatSortByChoice(enum.Enum):
843+
heat = enum.auto()
844+
table = enum.auto()
845+
node = enum.auto()
846+
847+
848+
class ShardHeatReporter:
843849
def __init__(self, analyzer: ShardAnalyzer):
844850
self.analyzer = analyzer
845-
self.reference_shards: dict[str, ShardInfo]
846-
self.latest_shards: list[ShardInfo]
847-
self.seq_deltas: dict[str, int]
848-
self.size_deltas: dict[str, float]
851+
self.reference_shards: dict[str, ShardInfo] = {}
852+
self.latest_shards: list[ShardInfo] = []
853+
self.seq_deltas: dict[str, int] = {}
854+
self.size_deltas: dict[str, float] = {}
849855

850-
self.table_filter: str|None = None
851-
self.sort_by: str = 'heat'
856+
self.table_filter: str | None = None
857+
self.sort_by: ShardHeatSortByChoice = ShardHeatSortByChoice.heat
852858

853-
def monitor_shards(self, table_filter: str | None, interval_in_seconds: int = 5, repeat: int = 10, n_shards: int = 40, sort_by: str = 'heat'):
859+
def report(
860+
self,
861+
table_filter: str | None,
862+
interval_in_seconds: int,
863+
watch: bool,
864+
n_shards: int,
865+
sort_by: ShardHeatSortByChoice,
866+
):
854867
self.table_filter = table_filter
855868
self.sort_by = sort_by
856869

857870
self.reference_shards = {self._get_shard_compound_id(shard): shard for shard in self.analyzer.shards}
871+
start_time = datetime.now()
858872
self.refresh_data()
859873

860-
console.print(Panel.fit(f"[bold blue]The {n_shards} Hottest Shards[/bold blue]"))
874+
console.print(Panel.fit("[bold blue]Shard heat analyzer[/bold blue]"))
861875

862-
iterations = 0
863876
while True:
864877
sleep(interval_in_seconds)
865878
self.refresh_data()
866-
shards_table = self.generate_shards_table(self._get_top_shards(self.latest_shards, n_shards), self.seq_deltas)
879+
shards_table = self.generate_shards_table(
880+
self._get_top_shards(self.latest_shards, n_shards),
881+
self.seq_deltas,
882+
(datetime.now() - start_time).total_seconds(),
883+
)
867884
console.print(shards_table)
868885
nodes_table = self.generate_nodes_table(self._get_nodes_heat_info(self.reference_shards, self.seq_deltas))
869886
console.print(nodes_table)
870887

871-
iterations += 1
872-
if 0 < repeat <= iterations:
888+
if not watch:
873889
break
874890

875-
def generate_nodes_table(self, heat_nodes_info: dict[str, int]):
891+
@staticmethod
892+
def generate_nodes_table(heat_nodes_info: dict[str, int]):
893+
console.print()
876894
table = Table(title="Shard heat by node", box=box.ROUNDED)
877895
table.add_column("Node name", style="cyan")
878896
table.add_column("Heat", style="magenta")
879897

880-
sorted_items = sorted(heat_nodes_info.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
898+
sorted_items = sorted(heat_nodes_info.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
881899

882900
for k, v in sorted_items:
883901
table.add_row(k, str(v))
884902

885903
return table
886904

887-
def generate_shards_table(self, sorted_shards: list[ShardInfo], deltas: dict[str, int]):
888-
t = self.display_shards_table_header()
889-
self.display_shards_table_rows(t, sorted_shards, deltas)
905+
def generate_shards_table(self, sorted_shards: list[ShardInfo], deltas: dict[str, int], elapsed_time_s: float):
906+
t = self._display_shards_table_header()
907+
self._display_shards_table_rows(t, sorted_shards, deltas, elapsed_time_s)
890908
return t
891909

892-
# Cluster summary table
893-
def display_shards_table_header(self):
894-
shards_table = Table(title="Hot shards", box=box.ROUNDED)
910+
def _display_shards_table_header(self):
911+
shards_table = Table(title=f"Shards sorted by {self.sort_by.name}", box=box.ROUNDED)
895912
shards_table.add_column("Schema", style="cyan")
896913
shards_table.add_column("Table", style="cyan")
897-
shards_table.add_column("ID", style="cyan")
914+
shards_table.add_column("Partition", style="cyan")
915+
shards_table.add_column("Shard ID", style="cyan")
898916
shards_table.add_column("Node", style="cyan")
899917
shards_table.add_column("Primary", style="cyan")
900918
shards_table.add_column("Size", style="magenta")
901919
shards_table.add_column("Size Delta", style="magenta")
902920
shards_table.add_column("Seq Delta", style="magenta")
921+
shards_table.add_column("ops/second", style="magenta")
903922
return shards_table
904923

905-
def display_shards_table_rows(self, shards_table: Table, sorted_shards: list[ShardInfo], deltas: dict[str, int]):
906-
shards_table.rows.clear()
907-
924+
def _display_shards_table_rows(
925+
self, shards_table: Table, sorted_shards: list[ShardInfo], deltas: dict[str, int], elapsed_time_s: float
926+
):
908927
for shard in sorted_shards:
909928
shard_compound_id = self._get_shard_compound_id(shard)
910929
seq_delta = deltas.get(shard_compound_id, 0)
911-
if seq_delta != 0:
912-
shards_table.add_row(
913-
shard.schema_name,
914-
shard.table_name,
915-
str(shard.shard_id),
916-
shard.node_name,
917-
str(shard.is_primary),
918-
format_size(shard.size_gb),
919-
format_size(self.size_deltas[shard_compound_id]),
920-
str(seq_delta)
921-
)
922-
console.print(shards_table)
930+
shards_table.add_row(
931+
shard.schema_name,
932+
shard.table_name,
933+
shard.partition_id,
934+
str(shard.shard_id),
935+
shard.node_name,
936+
str(shard.is_primary),
937+
format_size(shard.size_gb),
938+
format_size(seq_delta),
939+
str(seq_delta),
940+
str(seq_delta / elapsed_time_s),
941+
)
923942

924943
def _get_shard_compound_id(self, shard: ShardInfo) -> str:
925-
if self.sort_by == 'node':
926-
return f"{shard.node_name}-{shard.table_name}-{shard.shard_id}"
944+
if self.sort_by == ShardHeatSortByChoice.node:
945+
return f"{shard.node_name}-{shard.table_name}-{shard.shard_id}-{shard.partition_id}"
927946
else:
928-
return f"{shard.table_name}-{shard.shard_id}-{shard.node_name}"
947+
return f"{shard.table_name}-{shard.shard_id}-{shard.node_name}-{shard.partition_id}"
929948

930949
def calculate_heat_deltas(self, reference_shards: dict[str, ShardInfo], updated_shards: list[ShardInfo]):
931950
seq_result: dict[str, int] = {}
@@ -943,7 +962,7 @@ def calculate_heat_deltas(self, reference_shards: dict[str, ShardInfo], updated_
943962
reference = reference_shards[shard_compound_id].seq_stats_max_seq_no
944963

945964
if refreshed_number < reference:
946-
refreshed_number += 2 ** 63 - 1
965+
refreshed_number += 2**63 - 1
947966

948967
seq_result[shard_compound_id] = refreshed_number - reference
949968
size_result[shard_compound_id] = shard.size_gb - reference_shards[shard_compound_id].size_gb
@@ -953,29 +972,33 @@ def calculate_heat_deltas(self, reference_shards: dict[str, ShardInfo], updated_
953972

954973
def refresh_data(self):
955974
self.analyzer._refresh_data()
956-
updated_shards: list[ShardInfo] = [s for s in self.analyzer.shards if not self.table_filter or self.table_filter == s.table_name]
975+
updated_shards: list[ShardInfo] = [
976+
s for s in self.analyzer.shards if not self.table_filter or self.table_filter == s.table_name
977+
]
957978
self.calculate_heat_deltas(self.reference_shards, updated_shards)
958-
if self.sort_by == 'heat':
959-
self.latest_shards = sorted(updated_shards, key=lambda s: self.seq_deltas[self._get_shard_compound_id(s)],
960-
reverse=True)
979+
if self.sort_by == ShardHeatSortByChoice.heat:
980+
self.latest_shards = sorted(
981+
updated_shards, key=lambda s: self.seq_deltas[self._get_shard_compound_id(s)], reverse=True
982+
)
961983
else:
962984
self.latest_shards = sorted(updated_shards, key=lambda s: self._get_shard_compound_id(s))
963985

964-
965986
def _get_top_shards(self, sorted_shards: list[ShardInfo], n_shards: int) -> list[ShardInfo]:
966-
if n_shards < 1:
987+
if n_shards > 0:
967988
return sorted_shards[:n_shards]
968989
else:
969990
return sorted_shards
970991

971992
def _get_nodes_heat_info(self, shards: dict[str, ShardInfo], seq_deltas: dict[str, int]) -> dict[str, int]:
972993
nodes: dict[str, int] = {}
973994
for k, v in seq_deltas.items():
974-
node_name = shards.get(k).node_name
975-
if node_name not in nodes:
976-
nodes[node_name] = v
977-
else:
978-
nodes[node_name] += v
995+
shard = shards.get(k)
996+
if shard:
997+
node_name = shard.node_name
998+
if node_name not in nodes:
999+
nodes[node_name] = v
1000+
else:
1001+
nodes[node_name] += v
9791002
return nodes
9801003

9811004

cratedb_toolkit/admin/xmover/cli.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
import click
1111
from rich.console import Console
1212

13-
from cratedb_toolkit.admin.xmover.analysis.shard import ShardAnalyzer, ShardReporter, ShardMonitor
13+
from cratedb_toolkit.admin.xmover.analysis.shard import (
14+
ShardAnalyzer,
15+
ShardHeatReporter,
16+
ShardHeatSortByChoice,
17+
ShardReporter,
18+
)
1419
from cratedb_toolkit.admin.xmover.analysis.table import DistributionAnalyzer
1520
from cratedb_toolkit.admin.xmover.analysis.zone import ZoneReport
1621
from cratedb_toolkit.admin.xmover.model import (
@@ -63,18 +68,31 @@ def analyze(ctx, table: Optional[str]):
6368

6469

6570
@main.command()
66-
@click.option("--table", "-t", default=None, help="Analyze specific table only")
67-
@click.option("--wait-time", "-w", default=10, help="The number of seconds to wait before checking the shards again. The more the wait the more accurate the results will be (default: 10)")
68-
@click.option("--repeat", "-r", default=1, help="The number of times the shards will be checked. The more times the more accurate the results will be. Use -1 for continuous check (default 1)")
71+
@click.option("--table", "-t", type=click.STRING, default=None, help="Analyze specific table only")
72+
@click.option(
73+
"--interval", "-i", default=10, help="The number of seconds to wait between shard data captures (default: 10)"
74+
)
75+
@click.option(
76+
"--watch",
77+
"-w",
78+
is_flag=True,
79+
help="When set the tool will endlessly check and report.",
80+
)
6981
@click.option("--max-results", "-m", default=40, help="The number of shards that will be displayed (default: 40)")
70-
@click.option("--sort-by", "-s", default="heat", help="How the shard table is sorted. Valid values are heat, node or table (default: heat)")
82+
@click.option(
83+
"--sort-by",
84+
"-s",
85+
type=click.Choice([option.name for option in ShardHeatSortByChoice]),
86+
default="heat",
87+
help="How the shard table is sorted. Valid values are heat, node or table (default: heat)",
88+
)
7189
@click.pass_context
72-
def monitor_shards(ctx, table, wait_time, repeat, max_results, sort_by):
90+
def shard_heat(ctx, table: str | None, interval: int, watch: bool, max_results: int, sort_by):
7391
"""Monitor shards, pointing out hot ones"""
7492
client = ctx.obj["client"]
7593
analyzer = ShardAnalyzer(client)
76-
monitor = ShardMonitor(analyzer)
77-
monitor.monitor_shards(table, wait_time, repeat, max_results, sort_by)
94+
reporter = ShardHeatReporter(analyzer)
95+
reporter.report(table, interval, watch, max_results, ShardHeatSortByChoice[sort_by])
7896

7997

8098
@main.command()

cratedb_toolkit/admin/xmover/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ShardInfo:
4848
seq_stats_max_seq_no: int
4949
seq_stats_global_checkpoint: int
5050
seq_stats_local_checkpoint: int
51+
partition_id: str
5152

5253
@property
5354
def shard_type(self) -> str:

cratedb_toolkit/admin/xmover/util/database.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def get_shards_info(
150150
s.routing_state,
151151
s.seq_no_stats['max_seq_no'],
152152
s.seq_no_stats['global_checkpoint'],
153-
s.seq_no_stats['local_checkpoint']
153+
s.seq_no_stats['local_checkpoint'],
154+
s.partition_ident
154155
FROM sys.shards s
155156
JOIN sys.nodes n ON s.node['id'] = n.id
156157
{where_clause}
@@ -178,6 +179,7 @@ def get_shards_info(
178179
seq_stats_max_seq_no=row[12],
179180
seq_stats_global_checkpoint=row[13],
180181
seq_stats_local_checkpoint=row[14],
182+
partition_id=row[15],
181183
)
182184
)
183185

0 commit comments

Comments
 (0)