Skip to content

Commit 36b468e

Browse files
xurui-cRachel Chengetsantry[bot]
authored
ref(cbrs): introduce RegisteredClass into ConfigurableComponent (#7379)
Retrieve configurable component by doing ## option 1 `ConfigurableComponent.get_component_class(namespace).get_from_name(class_name)` ex) `ConfigurableComponent.get_component_class("AllocationPolicy").get_from_name("BytesScannedWindowPolicy")` ## option 2 `AllocationPolicy.get_from_name(class_name)` ex) `AllocationPolicy.get_from_name("BytesScannedWindowPolicy)` Option 1 is used for the set configuration endpoint - the frontend code will pass in the namespace and the class name, so use option 1 to get the configurable component Option 2 is already used in existing code so I'm going to preserve that Note: When you print out `ConfigurableComponent.all_names()`, you get ``` ['AllocationPolicy.AllocationPolicy', 'AllocationPolicy.PassthroughPolicy', 'AllocationPolicy.BaseConcurrentRateLimitAllocationPolicy', 'AllocationPolicy.ConcurrentRateLimitAllocationPolicy', 'AllocationPolicy.DeleteConcurrentRateLimitAllocationPolicy', 'AllocationPolicy.ReferrerGuardRailPolicy', 'AllocationPolicy.CrossOrgQueryAllocationPolicy', 'AllocationPolicy.BytesScannedRejectingPolicy', 'AllocationPolicy.BytesScannedWindowAllocationPolicy', 'RoutingStrategy.BaseRoutingStrategy', 'RoutingStrategy.OutcomesBasedRoutingStrategy'] ``` At one point I did think about doing `ConfigurableComponent.get_from_name(namespace, class_name)`, similar to [RPCEndpoint.get_from_name(name, version)](https://github.com/getsentry/snuba/blob/master/snuba/web/rpc/__init__.py#L133-L137). But I decided against it because we either change existing `AllocationPolicy.get_from_name(blah)` to `ConfigurableComponent.get_from_name("AllocationPolicy", "blah")` throughout the code base, or within each subclass's `get_from_name` method, we call `super().get_from_name(subclass's namespace, name)`, and then we cast it to type subclass, and we make namespace an optional param. Maybe this works, but I already coded up the existing approach and I don't see a clear advantage here --------- Co-authored-by: Rachel Chen <[email protected]> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 0be5528 commit 36b468e

File tree

9 files changed

+129
-235
lines changed

9 files changed

+129
-235
lines changed

snuba/admin/views.py

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,7 @@ def settings_endpoint() -> Response:
175175

176176
@application.route("/tools")
177177
def tools() -> Response:
178-
return make_response(
179-
jsonify({"tools": [t.value for t in get_user_allowed_tools(g.user)]}), 200
180-
)
178+
return make_response(jsonify({"tools": [t.value for t in get_user_allowed_tools(g.user)]}), 200)
181179

182180

183181
@application.route("/migrations/groups")
@@ -225,9 +223,7 @@ def migrations_groups_list(group: str) -> Response:
225223
)
226224
@check_tool_perms(tools=[AdminTools.MIGRATIONS])
227225
def run_migration(group: str, migration_id: str) -> Response:
228-
return run_or_reverse_migration(
229-
group=group, action="run", migration_id=migration_id
230-
)
226+
return run_or_reverse_migration(group=group, action="run", migration_id=migration_id)
231227

232228

233229
@application.route(
@@ -236,19 +232,15 @@ def run_migration(group: str, migration_id: str) -> Response:
236232
)
237233
@check_tool_perms(tools=[AdminTools.MIGRATIONS])
238234
def reverse_migration(group: str, migration_id: str) -> Response:
239-
return run_or_reverse_migration(
240-
group=group, action="reverse", migration_id=migration_id
241-
)
235+
return run_or_reverse_migration(group=group, action="reverse", migration_id=migration_id)
242236

243237

244238
@application.route(
245239
"/migrations/<group>/overwrite/<migration_id>/status/<new_status>",
246240
methods=["POST"],
247241
)
248242
@check_tool_perms(tools=[AdminTools.MIGRATIONS])
249-
def force_overwrite_migration_status(
250-
group: str, migration_id: str, new_status: str
251-
) -> Response:
243+
def force_overwrite_migration_status(group: str, migration_id: str, new_status: str) -> Response:
252244
try:
253245
migration_group = MigrationGroup(group)
254246
except ValueError as err:
@@ -302,9 +294,7 @@ def do_action() -> None:
302294
if action == "run":
303295
runner.run_migration(migration_key, force=force, fake=fake, dry_run=dry_run)
304296
else:
305-
runner.reverse_migration(
306-
migration_key, force=force, fake=fake, dry_run=dry_run
307-
)
297+
runner.reverse_migration(migration_key, force=force, fake=fake, dry_run=dry_run)
308298

309299
if not dry_run:
310300
audit_log.record(
@@ -356,15 +346,11 @@ def write(self, s: str) -> int:
356346
except ClickhouseError as err:
357347
notify_error()
358348
logger.error(err, exc_info=True)
359-
return make_response(
360-
jsonify({"error": "clickhouse error: " + err.message}), 400
361-
)
349+
return make_response(jsonify({"error": "clickhouse error: " + err.message}), 400)
362350
except InactiveClickhouseReplica as err:
363351
notify_error()
364352
logger.error(err, exc_info=True)
365-
return make_response(
366-
jsonify({"error": "inactive replicas error: " + err.message}), 400
367-
)
353+
return make_response(jsonify({"error": "inactive replicas error: " + err.message}), 400)
368354

369355

370356
@application.route("/clickhouse_queries")
@@ -432,9 +418,7 @@ def clickhouse_system_query() -> Response:
432418
return make_response(jsonify({"error": "Invalid request"}), 400)
433419

434420
try:
435-
result = run_system_query_on_host_with_sql(
436-
host, port, storage, raw_sql, sudo_mode, g.user
437-
)
421+
result = run_system_query_on_host_with_sql(host, port, storage, raw_sql, sudo_mode, g.user)
438422
rows = []
439423
rows, columns = cast(List[List[str]], result.results), result.meta
440424

@@ -600,16 +584,12 @@ def summarize_trace_with_profile() -> Response:
600584
except ClickhouseError as err:
601585
logger.error(err, exc_info=True)
602586
return make_response(
603-
jsonify(
604-
{"error": {"type": "clickhouse", "message": str(err), "code": err.code}}
605-
),
587+
jsonify({"error": {"type": "clickhouse", "message": str(err), "code": err.code}}),
606588
400,
607589
)
608590
except Exception as err:
609591
logger.error(err, exc_info=True)
610-
return make_response(
611-
jsonify({"error": {"type": "unknown", "message": str(err)}}), 500
612-
)
592+
return make_response(jsonify({"error": {"type": "unknown", "message": str(err)}}), 500)
613593

614594

615595
@application.route("/clickhouse_querylog_query", methods=["POST"])
@@ -910,18 +890,14 @@ def serialize(
910890
@application.route("/clickhouse_nodes")
911891
@check_tool_perms(tools=[AdminTools.SYSTEM_QUERIES, AdminTools.QUERY_TRACING])
912892
def clickhouse_nodes() -> Response:
913-
return Response(
914-
json.dumps(get_storage_info()), 200, {"Content-Type": "application/json"}
915-
)
893+
return Response(json.dumps(get_storage_info()), 200, {"Content-Type": "application/json"})
916894

917895

918896
@application.route("/snuba_datasets")
919897
@check_tool_perms(tools=[AdminTools.SNQL_TO_SQL])
920898
def snuba_datasets() -> Response:
921899
return Response(
922-
json.dumps(
923-
get_enabled_dataset_names(), 200, {"Content-Type": "application/json"}
924-
)
900+
json.dumps(get_enabled_dataset_names(), 200, {"Content-Type": "application/json"})
925901
)
926902

927903

@@ -972,9 +948,7 @@ def storages_with_allocation_policies() -> Response:
972948
@check_tool_perms(tools=[AdminTools.CAPACITY_MANAGEMENT])
973949
def get_allocation_policy_configs(storage_key: str) -> Response:
974950
storage = get_storage(StorageKey(storage_key))
975-
policies = (
976-
storage.get_allocation_policies() + storage.get_delete_allocation_policies()
977-
)
951+
policies = storage.get_allocation_policies() + storage.get_delete_allocation_policies()
978952

979953
return Response(
980954
json.dumps([convert(policy.to_dict()) for policy in policies]),
@@ -1005,7 +979,7 @@ def set_allocation_policy_config() -> Response:
1005979
+ get_storage(StorageKey(storage)).get_delete_allocation_policies()
1006980
)
1007981
policy = next(
1008-
(p for p in policies if p.config_key() == policy_name),
982+
(p for p in policies if p.class_name() == policy_name),
1009983
None,
1010984
)
1011985
assert policy is not None, "Policy not found on storage"
@@ -1022,23 +996,21 @@ def set_allocation_policy_config() -> Response:
1022996
audit_log.record(
1023997
user or "",
1024998
AuditLogAction.ALLOCATION_POLICY_DELETE,
1025-
{"storage": storage, "policy": policy.config_key(), "key": key},
999+
{"storage": storage, "policy": policy.class_name(), "key": key},
10261000
notify=True,
10271001
)
10281002
return Response("", 200)
10291003
elif request.method == "POST":
10301004
try:
10311005
value = data["value"]
10321006
assert isinstance(value, str), "Invalid value"
1033-
policy.set_config_value(
1034-
config_key=key, value=value, params=params, user=user
1035-
)
1007+
policy.set_config_value(config_key=key, value=value, params=params, user=user)
10361008
audit_log.record(
10371009
user or "",
10381010
AuditLogAction.ALLOCATION_POLICY_UPDATE,
10391011
{
10401012
"storage": storage,
1041-
"policy": policy.config_key(),
1013+
"policy": policy.class_name(),
10421014
"key": key,
10431015
"value": value,
10441016
"params": str(params),
@@ -1192,9 +1164,7 @@ def execute_rpc_endpoint(endpoint_name: str, version: str) -> Response:
11921164
)
11931165
except InvalidConfigKeyError:
11941166
return Response(
1195-
json.dumps(
1196-
{"error": f"Unknown endpoint: {endpoint_name} or version: {version}"}
1197-
),
1167+
json.dumps({"error": f"Unknown endpoint: {endpoint_name} or version: {version}"}),
11981168
404,
11991169
{"Content-Type": "application/json"},
12001170
)
@@ -1204,9 +1174,7 @@ def execute_rpc_endpoint(endpoint_name: str, version: str) -> Response:
12041174
try:
12051175
request_proto = Parse(json.dumps(body), endpoint_class.request_class()())
12061176
validate_request_meta(request_proto)
1207-
response = run_rpc_handler(
1208-
endpoint_name, version, request_proto.SerializeToString()
1209-
)
1177+
response = run_rpc_handler(endpoint_name, version, request_proto.SerializeToString())
12101178
return Response(
12111179
json.dumps(MessageToDict(response)),
12121180
200,
@@ -1333,9 +1301,7 @@ def delete() -> Response:
13331301
sentry_sdk.capture_exception(e)
13341302
return make_response(jsonify({"error": "unexpected internal error"}), 500)
13351303

1336-
return Response(
1337-
json.dumps(delete_results), 200, {"Content-Type": "application/json"}
1338-
)
1304+
return Response(json.dumps(delete_results), 200, {"Content-Type": "application/json"})
13391305

13401306

13411307
@application.route(
@@ -1396,9 +1362,7 @@ def clickhouse_system_settings() -> Response:
13961362
port = request.args.get("port")
13971363
storage = request.args.get("storage")
13981364
if not all([host, port, storage]):
1399-
return make_response(
1400-
jsonify({"error": "Host, port, and storage are required"}), 400
1401-
)
1365+
return make_response(jsonify({"error": "Host, port, and storage are required"}), 400)
14021366
try:
14031367
# conversions for typing
14041368
settings = get_system_settings(str(host), int(str(port)), str(storage))

snuba/configs/configuration.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import logging
22
from abc import ABC, abstractmethod
33
from dataclasses import dataclass, field, replace
4-
from typing import Any, TypedDict, final
4+
from typing import Any, Type, TypedDict, TypeVar, cast, final
55

66
from snuba.datasets.storages.storage_key import StorageKey
77
from snuba.state import delete_config as delete_runtime_config
88
from snuba.state import get_all_configs as get_all_runtime_configs
99
from snuba.state import get_config as get_runtime_config
1010
from snuba.state import set_config as set_runtime_config
11+
from snuba.utils.registered_class import RegisteredClass
1112

1213
logger = logging.getLogger("snuba.configurable_component")
1314

15+
T = TypeVar("T", bound="ConfigurableComponent")
16+
1417

1518
class InvalidConfig(Exception):
1619
pass
@@ -60,7 +63,7 @@ class Configuration:
6063
param_types: dict[str, type] = field(default_factory=dict)
6164

6265
def __post_init__(self) -> None:
63-
if type(self.default) != self.value_type:
66+
if type(self.default) is not self.value_type:
6467
raise ValueError(
6568
f"Config item `{self.name}` expects type {self.value_type} got value `{self.default}` of type {type(self.default)}"
6669
)
@@ -83,9 +86,7 @@ def to_definition_dict(self) -> dict[str, Any]:
8386
],
8487
}
8588

86-
def to_config_dict(
87-
self, value: Any = None, params: dict[str, Any] = {}
88-
) -> dict[str, Any]:
89+
def to_config_dict(self, value: Any = None, params: dict[str, Any] = {}) -> dict[str, Any]:
8990
"""Returns a dict representation of a live Config."""
9091
return {
9192
**self.__to_base_dict(),
@@ -94,7 +95,7 @@ def to_config_dict(
9495
}
9596

9697

97-
class ConfigurableComponent(ABC):
98+
class ConfigurableComponent(ABC, metaclass=RegisteredClass):
9899
"""
99100
A ConfigurableComponent is a component that can be configured via configurations.
100101
example: an allocation policy, a routing strategy, a strategy selector.
@@ -148,7 +149,8 @@ def component_name(self) -> str:
148149
# needs to uniquely identify the configurable component
149150
return f"{self.resource_identifier.value}.{self.__class__.__name__}"
150151

151-
def component_namespace(self) -> str:
152+
@classmethod
153+
def component_namespace(cls) -> str:
152154
# is it an allocation policy? a routing strategy? a strategy selector?
153155
raise NotImplementedError
154156

@@ -191,9 +193,7 @@ def _validate_config_params(
191193

192194
# config doesn't exist
193195
if config_key not in definitions:
194-
raise InvalidConfig(
195-
f"'{config_key}' is not a valid config for {class_name}!"
196-
)
196+
raise InvalidConfig(f"'{config_key}' is not a valid config for {class_name}!")
197197

198198
config = definitions[config_key]
199199

@@ -335,9 +335,7 @@ def _get_overridden_additional_config_defaults(
335335
return [
336336
replace(
337337
definition,
338-
default=default_config_overrides.get(
339-
definition.name, definition.default
340-
),
338+
default=default_config_overrides.get(definition.name, definition.default),
341339
)
342340
for definition in definitions
343341
]
@@ -350,9 +348,7 @@ def __escape_delimiter_chars(self, key: str) -> str:
350348
escape_sequence,
351349
) in self._KEY_DELIMITERS_TO_ESCAPE_SEQUENCES.items():
352350
if escape_sequence in str(key):
353-
raise InvalidConfig(
354-
f"{escape_sequence} is not a valid string for a policy config"
355-
)
351+
raise InvalidConfig(f"{escape_sequence} is not a valid string for a policy config")
356352
key = key.replace(delimiter_char, escape_sequence)
357353
return key
358354

@@ -431,13 +427,28 @@ def delete_config_value(
431427

432428
@classmethod
433429
def config_key(cls) -> str:
430+
return f"{cls.component_namespace()}.{cls.__name__}"
431+
432+
@classmethod
433+
def class_name(cls) -> str:
434434
return cls.__name__
435435

436436
def to_dict(self) -> ConfigurableComponentData:
437437
return ConfigurableComponentData(
438438
configurable_component_namespace=self.component_namespace(),
439-
configurable_component_config_key=self.config_key(),
439+
configurable_component_config_key=self.class_name(),
440440
resource_identifier=self.resource_identifier.value,
441441
configurations=self.get_current_configs(),
442442
optional_config_definitions=self.get_optional_config_definitions_json(),
443443
)
444+
445+
@classmethod
446+
def get_component_class(cls, namespace: str) -> Type["ConfigurableComponent"]:
447+
return cast(
448+
Type["ConfigurableComponent"],
449+
cls.class_from_name(f"{namespace}.{namespace}"),
450+
)
451+
452+
@classmethod
453+
def get_from_name(cls: Type[T], name: str) -> Type[T]:
454+
return cast(Type[T], cls.class_from_name(f"{cls.component_namespace()}.{name}"))

snuba/query/allocation_policies/__init__.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from snuba.datasets.storages.storage_key import StorageKey
1818
from snuba.utils.metrics.wrapper import MetricsWrapper
19-
from snuba.utils.registered_class import RegisteredClass, import_submodules_in_directory
19+
from snuba.utils.registered_class import import_submodules_in_directory
2020
from snuba.utils.serializable_exception import JsonSerializable, SerializableException
2121
from snuba.web import QueryException, QueryResult
2222

@@ -127,9 +127,7 @@ def violations(self) -> dict[str, dict[str, Any]]:
127127

128128
@property
129129
def quota_allowance(self) -> dict[str, dict[str, Any]]:
130-
return cast(
131-
dict[str, dict[str, Any]], self.extra_data.get("quota_allowances", {})
132-
)
130+
return cast(dict[str, dict[str, Any]], self.extra_data.get("quota_allowances", {}))
133131

134132
@property
135133
def summary(self) -> dict[str, Any]:
@@ -155,7 +153,7 @@ class QueryType(Enum):
155153
DELETE = "delete"
156154

157155

158-
class AllocationPolicy(ConfigurableComponent, ABC, metaclass=RegisteredClass):
156+
class AllocationPolicy(ConfigurableComponent, ABC):
159157
"""This class should be the centralized place for policy decisions regarding
160158
resource usage of a clickhouse cluster. It is meant to live as a configurable item
161159
on a storage.
@@ -373,7 +371,8 @@ def __init__(
373371
self._get_overridden_additional_config_defaults(default_config_overrides)
374372
)
375373

376-
def component_namespace(self) -> str:
374+
@classmethod
375+
def component_namespace(cls) -> str:
377376
return "AllocationPolicy"
378377

379378
def _get_hash(self) -> str:
@@ -393,10 +392,7 @@ def metrics(self) -> MetricsWrapper:
393392

394393
@property
395394
def is_active(self) -> bool:
396-
return (
397-
bool(self.get_config_value(IS_ACTIVE))
398-
and settings.ALLOCATION_POLICY_ENABLED
399-
)
395+
return bool(self.get_config_value(IS_ACTIVE)) and settings.ALLOCATION_POLICY_ENABLED
400396

401397
@property
402398
def is_enforced(self) -> bool:
@@ -407,10 +403,6 @@ def max_threads(self) -> int:
407403
"""Maximum number of threads run a single query on ClickHouse with."""
408404
return int(self.get_config_value(MAX_THREADS))
409405

410-
@classmethod
411-
def get_from_name(cls, name: str) -> "AllocationPolicy":
412-
return cast("AllocationPolicy", cls.class_from_name(name))
413-
414406
def __eq__(self, other: Any) -> bool:
415407
"""There should not be a need to compare these except that
416408
AllocationPolicies are attached to the Table a query is executed against.

0 commit comments

Comments
 (0)