Skip to content

Conversation

@xurui-c
Copy link
Member

@xurui-c xurui-c commented Aug 31, 2025

Allocation policies should be aware of whether they are a "select" policy or a "delete" policy, so that allocation policy payloads can be constructed from the object itself.

This PR also integrates resource identifiers into allocation policies and because allocation policies are no longer goverened by only storage keys, we get rid of the storage_key property

@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch 2 times, most recently from cef6448 to 920e872 Compare August 31, 2025 03:52
@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch from 920e872 to 36509d4 Compare August 31, 2025 03:56

def to_dict(self) -> PolicyData:
base_data = super().to_dict()
return PolicyData(**base_data, query_type=self.query_type.value) # type: ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without # type: ignore, it says Returning Any from function declared to return "PolicyData". This is because mypy can't guarantee that **base_data will satisfy all PolicyData requirements

Alternatively, I can just do

base_data = super().to_dict()
return PolicyData(
        configurable_component_namespace=base_data["configurable_component_namespace"],
        configurable_component_config_key=base_data["configurable_component_config_key"],
        resource_identifier=base_data["resource_identifier"],
        configurations=base_data["configurations"],
        optional_config_definitions=base_data["optional_config_definitions"],
        query_type=self.query_type.value,
    )

@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch 2 times, most recently from 3ee6f42 to fdfde20 Compare August 31, 2025 04:41
@codecov
Copy link

codecov bot commented Aug 31, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1150 1 1149 7
View the top 2 failed test(s) by shortest run time
tests.clickhouse.test_native::test_concurrency_limit
Stack Traces | 0.013s run time
Traceback (most recent call last):
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 341, in from_call
    result: TResult | None = func()
                             ^^^^^^
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 242, in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 182, in _multicall
    return outcome.get_result()
           ^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.../site-packages/pluggy/_result.py", line 100, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/threadexception.py", line 92, in pytest_runtest_call
    yield from thread_exception_runtest_hook()
  File ".../local/lib/python3.11....../site-packages/_pytest/threadexception.py", line 68, in thread_exception_runtest_hook
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/unraisableexception.py", line 95, in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
  File ".../local/lib/python3.11....../site-packages/_pytest/unraisableexception.py", line 70, in unraisable_exception_runtest_hook
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/logging.py", line 846, in pytest_runtest_call
    yield from self._runtest_for(item, "call")
  File ".../local/lib/python3.11....../site-packages/_pytest/logging.py", line 829, in _runtest_for
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11.../site-packages/_pytest/capture.py", line 880, in pytest_runtest_call
    return (yield)
            ^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11.../site-packages/_pytest/skipping.py", line 257, in pytest_runtest_call
    return (yield)
            ^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 174, in pytest_runtest_call
    item.runtest()
  File ".../local/lib/python3.11....../site-packages/_pytest/python.py", line 1627, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File ".../local/lib/python3.11....../site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/_pytest/python.py", line 159, in pytest_pyfunc_call
    result = testfunction(**testargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../tests/clickhouse/test_native.py", line 82, in test_concurrency_limit
    assert connection.execute.call_count == 2, "Expected two attempts"
AssertionError: Expected two attempts
assert 1 == 2
 +  where 1 = <Mock name='mock.execute' id='140318384686544'>.call_count
 +    where <Mock name='mock.execute' id='140318384686544'> = <Mock id='140318384695888'>.execute
tests.query.allocation_policies.test_bytes_scanned_rejecting_policy::test_consume_quota[choose project_id over organization_id]
Stack Traces | 0.032s run time
Traceback (most recent call last):
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 341, in from_call
    result: TResult | None = func()
                             ^^^^^^
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 242, in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 182, in _multicall
    return outcome.get_result()
           ^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.../site-packages/pluggy/_result.py", line 100, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/threadexception.py", line 92, in pytest_runtest_call
    yield from thread_exception_runtest_hook()
  File ".../local/lib/python3.11....../site-packages/_pytest/threadexception.py", line 68, in thread_exception_runtest_hook
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/unraisableexception.py", line 95, in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
  File ".../local/lib/python3.11....../site-packages/_pytest/unraisableexception.py", line 70, in unraisable_exception_runtest_hook
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11....../site-packages/_pytest/logging.py", line 846, in pytest_runtest_call
    yield from self._runtest_for(item, "call")
  File ".../local/lib/python3.11....../site-packages/_pytest/logging.py", line 829, in _runtest_for
    yield
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11.../site-packages/_pytest/capture.py", line 880, in pytest_runtest_call
    return (yield)
            ^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 167, in _multicall
    teardown.throw(outcome._exception)
  File ".../local/lib/python3.11.../site-packages/_pytest/skipping.py", line 257, in pytest_runtest_call
    return (yield)
            ^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11........./site-packages/_pytest/runner.py", line 174, in pytest_runtest_call
    item.runtest()
  File ".../local/lib/python3.11....../site-packages/_pytest/python.py", line 1627, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File ".../local/lib/python3.11....../site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File ".../local/lib/python3.11.........................../site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../local/lib/python3.11....../site-packages/_pytest/python.py", line 159, in pytest_pyfunc_call
    result = testfunction(**testargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../query/allocation_policies/test_bytes_scanned_rejecting_policy.py", line 93, in test_consume_quota
    assert {
AssertionError: assert dict_items([('granted_quota', 0), ('limit', 1000), ('storage_key', 'StorageKey.ERRORS')]) <= dict_items([('reason', 'project_id 12345 is over the bytes scanned limit of 1000 for referrer some_referrer.\n                This policy is exceeded when a customer is abusing a specific feature in a way that puts load on clickhouse. If this is happening to\n                "many customers, that may mean the feature is written in an inefficient way'), ('granted_quota', 0), ('limit', 1000), ('storage_key', 'errors')])
 +  where dict_items([('granted_quota', 0), ('limit', 1000), ('storage_key', 'StorageKey.ERRORS')]) = <built-in method items of dict object at 0x7f15e6735cc0>()
 +    where <built-in method items of dict object at 0x7f15e6735cc0> = {'granted_quota': 0, 'limit': 1000, 'storage_key': 'StorageKey.ERRORS'}.items
 +  and   dict_items([('reason', 'project_id 12345 is over the bytes scanned limit of 1000 for referrer some_referrer.\n                This policy is exceeded when a customer is abusing a specific feature in a way that puts load on clickhouse. If this is happening to\n                "many customers, that may mean the feature is written in an inefficient way'), ('granted_quota', 0), ('limit', 1000), ('storage_key', 'errors')]) = <built-in method items of dict object at 0x7f15e8769bc0>()
 +    where <built-in method items of dict object at 0x7f15e8769bc0> = {'granted_quota': 0, 'limit': 1000, 'reason': 'project_id 12345 is over the bytes scanned limit of 1000 for referrer some_referrer.\n                This policy is exceeded when a customer is abusing a specific feature in a way that puts load on clickhouse. If this is happening to\n                "many customers, that may mean the feature is written in an inefficient way', 'storage_key': 'errors'}.items
 +      where {'granted_quota': 0, 'limit': 1000, 'reason': 'project_id 12345 is over the bytes scanned limit of 1000 for referrer some_referrer.\n                This policy is exceeded when a customer is abusing a specific feature in a way that puts load on clickhouse. If this is happening to\n                "many customers, that may mean the feature is written in an inefficient way', 'storage_key': 'errors'} = QuotaAllowance(can_run=False, max_threads=0, explanation={'reason': 'project_id 12345 is over the bytes scanned limit of 1000 for referrer some_referrer.\n                This policy is exceeded when a customer is abusing a specific feature in a way that puts load on clickhouse. If this is happening to\n                "many customers, that may mean the feature is written in an inefficient way', 'granted_quota': 0, 'limit': 1000, 'storage_key': 'errors'}, is_throttled=True, throttle_threshold=666, rejection_threshold=1000, quota_used=1000, quota_unit='bytes', suggestion='The feature, organization/project is scanning too many bytes, this usually means they are abusing that API', max_bytes_to_read=0).explanation

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch from fdfde20 to d1f1ec2 Compare August 31, 2025 05:17

class ConfigurableComponentData(TypedDict):
configurable_component_namespace: str
configurable_component_config_key: str
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know at one point we thought that we should set component_name to the class name ie "BytesScannedWindowAllocationPolicy", so we wanted to "flip" component_name and config_key, but RegisteredClass has config_key representing the name of the class so I'd rather be consistent

@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch from d1f1ec2 to bc89713 Compare August 31, 2025 05:47
quota_info["quota_used"] = quota_allowance.quota_used
quota_info["quota_unit"] = quota_allowance.quota_unit
quota_info["suggestion"] = quota_allowance.suggestion
quota_info["storage_key"] = str(quota_and_policy.policy.storage_key)
Copy link
Member Author

@xurui-c xurui-c Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in person, there was no vision between when using str(storage_key) (ie StorageKey.ERRORS) vs storage_key.value (ie errors), so I made everything use storage_key.value

If I don't do this, then ResourceIdentifier would have to keep track of the type of the underlying resource, because if the resource is StorageKey, then we'll want to use StoragaeKey's __repr__ method.

ResourceIdentifier should just take a string and when you pass in the storage key, convert to a string

@xurui-c xurui-c force-pushed the rachel/allocationpolicyref branch from f59dd2a to 343cd6d Compare September 1, 2025 01:03
)
# make sure we always know which storage key we rejected a query from
allowance.explanation["storage_key"] = str(self._storage_key)
allowance.explanation["storage_key"] = self._resource_identifier.value
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could change the key to be "resource_identifier" instead of "storage_key" but I find it'd be more confusing to have "resource_identifier" in querylog/payloads but then "storage_key" in DD tags (changing the latter, I assume, would break a bunch of DD metrics and I'd rather move the project along...)

@xurui-c xurui-c marked this pull request as ready for review September 2, 2025 16:00
@xurui-c xurui-c requested review from a team as code owners September 2, 2025 16:00
@property
def resource_identifier(self) -> ResourceIdentifier:
return ResourceIdentifier(self._storage_key)
def query_type(self) -> QueryType:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debated having query_type be an argument to pass into the constructor, but AFAIK the only delete policy that exists is DeleteConcurrentRateLimitAllocationPolicy so this is the simplest way to change this

Copy link
Member

@volokluev volokluev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved with one question

Comment on lines +432 to +434
@classmethod
def config_key(cls) -> str:
return cls.__name__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you said that RegisteredClass also has a config_key method but it represents a different thing. Maybe this function should be called something else that's more decriptive?

@xurui-c xurui-c merged commit 72b87af into master Sep 2, 2025
35 checks passed
@xurui-c xurui-c deleted the rachel/allocationpolicyref branch September 2, 2025 21:02
xurui-c added a commit that referenced this pull request Sep 8, 2025
https://linear.app/getsentry/issue/EAP-77/build-configuration-apis-for-routing-strategy

This PR builds the necessary APIs and endpoints in Snuba Admin to
retrieve configure routing strategies. This won't introduce any visual
changes yet since it just establishes the endpoints

DO NOT MERGE UNTIL: #7379,
#7375

---------

Co-authored-by: Rachel Chen <rachelchen@PL6VFX9HP4.local>
Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants