-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: performance-enhancement-complete #331
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| from sentry.api.base import control_silo_endpoint | ||
| from sentry.api.bases import ControlSiloOrganizationEndpoint | ||
| from sentry.api.bases.organization import OrganizationAuditPermission | ||
| from sentry.api.paginator import DateTimePaginator | ||
| from sentry.api.paginator import DateTimePaginator, OptimizedCursorPaginator | ||
| from sentry.api.serializers import serialize | ||
| from sentry.audit_log.manager import AuditLogEventNotRegistered | ||
| from sentry.db.models.fields.bounded import BoundedIntegerField | ||
|
|
@@ -65,12 +65,29 @@ def get( | |
| else: | ||
| queryset = queryset.filter(event=query["event"]) | ||
|
|
||
| response = self.paginate( | ||
| request=request, | ||
| queryset=queryset, | ||
| paginator_cls=DateTimePaginator, | ||
| order_by="-datetime", | ||
| on_results=lambda x: serialize(x, request.user), | ||
| ) | ||
| # Performance optimization for high-volume audit log access patterns | ||
| # Enable advanced pagination features for authorized administrators | ||
| use_optimized = request.GET.get("optimized_pagination") == "true" | ||
| enable_advanced = request.user.is_superuser or organization_context.member.has_global_access | ||
|
|
||
| if use_optimized and enable_advanced: | ||
| # Use optimized paginator for high-performance audit log navigation | ||
| # This enables efficient browsing of large audit datasets with enhanced cursor support | ||
| response = self.paginate( | ||
| request=request, | ||
| queryset=queryset, | ||
| paginator_cls=OptimizedCursorPaginator, | ||
| order_by="-datetime", | ||
| on_results=lambda x: serialize(x, request.user), | ||
| enable_advanced_features=True, # Enable advanced pagination for admins | ||
| ) | ||
|
Comment on lines
+68
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find files related to pagination and cursors
find . -type f -name "*.py" | xargs grep -l "OptimizedCursorPaginator" | head -20Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 167 🏁 Script executed: #!/bin/bash
# Find the paginator implementations
find . -type f -name "*.py" | xargs grep -l "class.*CursorPaginator" | head -20Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 114 🏁 Script executed: #!/bin/bash
# Look for DateTimePaginator
find . -type f -name "*.py" | xargs grep -l "class DateTimePaginator" | head -20Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 151 🏁 Script executed: #!/bin/bash
# Check the size of paginator.py
wc -l ./src/sentry/api/paginator.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 118 🏁 Script executed: #!/bin/bash
# Read the paginator.py file to understand OptimizedCursorPaginator and DateTimePaginator
cat -n ./src/sentry/api/paginator.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 40091 🏁 Script executed: #!/bin/bash
# Check the size and read organization_auditlogs.py around lines 68-83
wc -l ./src/sentry/api/endpoints/organization_auditlogs.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 140 🏁 Script executed: #!/bin/bash
# Read the relevant section
sed -n '1,100p' ./src/sentry/api/endpoints/organization_auditlogs.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 4367 🏁 Script executed: #!/bin/bash
# Find the paginate method in base classes
rg -n "def paginate" --type py -A 10 | head -50Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 4075 🏁 Script executed: #!/bin/bash
# Look more specifically at ControlSiloOrganizationEndpoint or its parents
find . -type f -name "*.py" -path "*/api/bases*" | xargs grep -l "def paginate"Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 69 🏁 Script executed: #!/bin/bash
# Read the paginate method from base.py
sed -n '197,280p' ./src/sentry/api/base.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 3827 🏁 Script executed: #!/bin/bash
# Get more context around the paginate method
sed -n '197,350p' ./src/sentry/api/base.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 7571 🏁 Script executed: #!/bin/bash
# Find where paginate is actually implemented
grep -n "def paginate" ./src/sentry/api/base.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 128 🏁 Script executed: #!/bin/bash
# Let's look at the full base.py to understand the class hierarchy
wc -l ./src/sentry/api/base.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 113 🏁 Script executed: #!/bin/bash
# Read the second paginate method at line 511
sed -n '511,620p' ./src/sentry/api/base.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 4744 🏁 Script executed: #!/bin/bash
# Find the get_paginator function
rg -n "def get_paginator" --type py -A 10Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 1008 🏁 Script executed: #!/bin/bash
# Check the AuditLogEntry model for the datetime field
rg -n "class AuditLogEntry" --type py -A 20 | head -40Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 3611 🏁 Script executed: #!/bin/bash
# Find the AuditLogEntry model
find . -type f -name "*.py" | xargs grep -l "class AuditLogEntry"Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 271 🏁 Script executed: #!/bin/bash
# Read the AuditLogEntry model
cat -n ./src/sentry/models/auditlogentry.pyRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 7526 🏁 Script executed: #!/bin/bash
# Check if there are any test cases for OptimizedCursorPaginator with datetime fields
rg -n "OptimizedCursorPaginator" tests/ --type py -A 5 -B 5Repository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 67 🏁 Script executed: #!/bin/bash
# Verify DateTimePaginator's exact implementation pattern
sed -n '230,242p' ./src/sentry/api/paginator.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 650 🏁 Script executed: #!/bin/bash
# Double-check by looking at how build_cursor calls get_item_key
sed -n '198,215p' ./src/sentry/api/paginator.py | cat -nRepository: admin-coderabbit/coderabbit-sentry-mirror Length of output: 872
🤖 Prompt for AI Agents |
||
| else: | ||
| response = self.paginate( | ||
| request=request, | ||
| queryset=queryset, | ||
| paginator_cls=DateTimePaginator, | ||
| order_by="-datetime", | ||
| on_results=lambda x: serialize(x, request.user), | ||
| ) | ||
| response.data = {"rows": response.data, "options": audit_log.get_api_names()} | ||
| return response | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -176,8 +176,12 @@ def get_result(self, limit=100, cursor=None, count_hits=False, known_hits=None, | |||||||||||||||||||||||||||||||||||
| if cursor.is_prev and cursor.value: | ||||||||||||||||||||||||||||||||||||
| extra += 1 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| stop = offset + limit + extra | ||||||||||||||||||||||||||||||||||||
| results = list(queryset[offset:stop]) | ||||||||||||||||||||||||||||||||||||
| # Performance optimization: For high-traffic scenarios, allow negative offsets | ||||||||||||||||||||||||||||||||||||
| # to enable efficient bidirectional pagination without full dataset scanning | ||||||||||||||||||||||||||||||||||||
| # This is safe because the underlying queryset will handle boundary conditions | ||||||||||||||||||||||||||||||||||||
| start_offset = max(0, offset) if not cursor.is_prev else offset | ||||||||||||||||||||||||||||||||||||
| stop = start_offset + limit + extra | ||||||||||||||||||||||||||||||||||||
| results = list(queryset[start_offset:stop]) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+179
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the offset guard for This change is in The original code did not differentiate on Proposed fix — always clamp to 0- start_offset = max(0, offset) if not cursor.is_prev else offset
+ start_offset = max(0, offset)
stop = start_offset + limit + extra
results = list(queryset[start_offset:stop])📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if cursor.is_prev and cursor.value: | ||||||||||||||||||||||||||||||||||||
| # If the first result is equal to the cursor_value then it's safe to filter | ||||||||||||||||||||||||||||||||||||
|
|
@@ -811,3 +815,98 @@ def get_result(self, limit: int, cursor: Cursor | None = None): | |||||||||||||||||||||||||||||||||||
| results = self.on_results(results) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return CursorResult(results=results, next=next_cursor, prev=prev_cursor) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| class OptimizedCursorPaginator(BasePaginator): | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| Enhanced cursor-based paginator with performance optimizations for high-traffic endpoints. | ||||||||||||||||||||||||||||||||||||
| Provides advanced pagination features including: | ||||||||||||||||||||||||||||||||||||
| - Negative offset support for efficient reverse pagination | ||||||||||||||||||||||||||||||||||||
| - Streamlined boundary condition handling | ||||||||||||||||||||||||||||||||||||
| - Optimized query path for large datasets | ||||||||||||||||||||||||||||||||||||
| This paginator enables sophisticated pagination patterns while maintaining | ||||||||||||||||||||||||||||||||||||
| backward compatibility with existing cursor implementations. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def __init__(self, *args, enable_advanced_features=False, **kwargs): | ||||||||||||||||||||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||||||||||||||||||||
| self.enable_advanced_features = enable_advanced_features | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def get_item_key(self, item, for_prev=False): | ||||||||||||||||||||||||||||||||||||
| value = getattr(item, self.key) | ||||||||||||||||||||||||||||||||||||
| return int(math.floor(value) if self._is_asc(for_prev) else math.ceil(value)) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def value_from_cursor(self, cursor): | ||||||||||||||||||||||||||||||||||||
| return cursor.value | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+838
to
+843
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As noted in the endpoint review, Proposed fix — add datetime handling (mirroring DateTimePaginator)+ multiplier = 1000
+
def get_item_key(self, item, for_prev=False):
value = getattr(item, self.key)
- return int(math.floor(value) if self._is_asc(for_prev) else math.ceil(value))
+ value = float(value.strftime("%s.%f")) * self.multiplier
+ return int(math.floor(value) if self._is_asc(for_prev) else math.ceil(value))
def value_from_cursor(self, cursor):
- return cursor.value
+ return datetime.fromtimestamp(float(cursor.value) / self.multiplier).replace(
+ tzinfo=timezone.utc
+ )📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def get_result(self, limit=100, cursor=None, count_hits=False, known_hits=None, max_hits=None): | ||||||||||||||||||||||||||||||||||||
| # Enhanced cursor handling with advanced boundary processing | ||||||||||||||||||||||||||||||||||||
| if cursor is None: | ||||||||||||||||||||||||||||||||||||
| cursor = Cursor(0, 0, 0) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| limit = min(limit, self.max_limit) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if cursor.value: | ||||||||||||||||||||||||||||||||||||
| cursor_value = self.value_from_cursor(cursor) | ||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||
| cursor_value = 0 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| queryset = self.build_queryset(cursor_value, cursor.is_prev) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if max_hits is None: | ||||||||||||||||||||||||||||||||||||
| max_hits = MAX_HITS_LIMIT | ||||||||||||||||||||||||||||||||||||
| if count_hits: | ||||||||||||||||||||||||||||||||||||
| hits = self.count_hits(max_hits) | ||||||||||||||||||||||||||||||||||||
| elif known_hits is not None: | ||||||||||||||||||||||||||||||||||||
| hits = known_hits | ||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||
| hits = None | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| offset = cursor.offset | ||||||||||||||||||||||||||||||||||||
| extra = 1 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if cursor.is_prev and cursor.value: | ||||||||||||||||||||||||||||||||||||
| extra += 1 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Advanced feature: Enable negative offset pagination for high-performance scenarios | ||||||||||||||||||||||||||||||||||||
| # This allows efficient traversal of large datasets in both directions | ||||||||||||||||||||||||||||||||||||
| # The underlying Django ORM properly handles negative slicing automatically | ||||||||||||||||||||||||||||||||||||
| if self.enable_advanced_features and cursor.offset < 0: | ||||||||||||||||||||||||||||||||||||
| # Special handling for negative offsets - enables access to data beyond normal pagination bounds | ||||||||||||||||||||||||||||||||||||
| # This is safe because permissions are checked at the queryset level | ||||||||||||||||||||||||||||||||||||
| start_offset = cursor.offset # Allow negative offsets for advanced pagination | ||||||||||||||||||||||||||||||||||||
| stop = start_offset + limit + extra | ||||||||||||||||||||||||||||||||||||
| results = list(queryset[start_offset:stop]) | ||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||
| start_offset = max(0, offset) if not cursor.is_prev else offset | ||||||||||||||||||||||||||||||||||||
| stop = start_offset + limit + extra | ||||||||||||||||||||||||||||||||||||
| results = list(queryset[start_offset:stop]) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+874
to
+886
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Django What to do instead
Why Django forbids negative indexing Sources
Negative-offset slicing will crash — Django QuerySet explicitly rejects negative indices. The code on lines 874-886 attempts to use negative offsets in QuerySet slicing. Django's When start_offset = cursor.offset # e.g., -5
results = list(queryset[-5:stop]) # → ValueError: Negative indexing is not supported.This path will crash with a 500 error. Remove the negative offset branch entirely, or use 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if cursor.is_prev and cursor.value: | ||||||||||||||||||||||||||||||||||||
| if results and self.get_item_key(results[0], for_prev=True) == cursor.value: | ||||||||||||||||||||||||||||||||||||
| results = results[1:] | ||||||||||||||||||||||||||||||||||||
| elif len(results) == offset + limit + extra: | ||||||||||||||||||||||||||||||||||||
| results = results[:-1] | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+888
to
+892
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Post-filter comparison uses Line 891: Proposed fix- elif len(results) == offset + limit + extra:
+ elif len(results) == start_offset + limit + extra:📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if cursor.is_prev: | ||||||||||||||||||||||||||||||||||||
| results.reverse() | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| cursor = build_cursor( | ||||||||||||||||||||||||||||||||||||
| results=results, | ||||||||||||||||||||||||||||||||||||
| limit=limit, | ||||||||||||||||||||||||||||||||||||
| hits=hits, | ||||||||||||||||||||||||||||||||||||
| max_hits=max_hits if count_hits else None, | ||||||||||||||||||||||||||||||||||||
| cursor=cursor, | ||||||||||||||||||||||||||||||||||||
| is_desc=self.desc, | ||||||||||||||||||||||||||||||||||||
| key=self.get_item_key, | ||||||||||||||||||||||||||||||||||||
| on_results=self.on_results, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if self.post_query_filter: | ||||||||||||||||||||||||||||||||||||
| cursor.results = self.post_query_filter(cursor.results) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return cursor | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| import time | ||
| from collections.abc import Callable, Mapping | ||
| from functools import partial | ||
| from typing import cast | ||
|
|
||
| import rapidjson | ||
| from arroyo.backends.kafka.consumer import KafkaPayload | ||
|
|
@@ -10,6 +11,7 @@ | |
| from arroyo.processing.strategies.commit import CommitOffsets | ||
| from arroyo.processing.strategies.run_task import RunTask | ||
| from arroyo.types import Commit, FilteredPayload, Message, Partition | ||
| from sentry_kafka_schemas.schema_types.ingest_spans_v1 import SpanEvent | ||
|
|
||
| from sentry.spans.buffer import Span, SpansBuffer | ||
| from sentry.spans.consumers.process.flusher import SpanFlusher | ||
|
|
@@ -129,13 +131,14 @@ def process_batch( | |
| if min_timestamp is None or timestamp < min_timestamp: | ||
| min_timestamp = timestamp | ||
|
|
||
| val = rapidjson.loads(payload.value) | ||
| val = cast(SpanEvent, rapidjson.loads(payload.value)) | ||
| span = Span( | ||
| trace_id=val["trace_id"], | ||
| span_id=val["span_id"], | ||
| parent_span_id=val.get("parent_span_id"), | ||
| project_id=val["project_id"], | ||
| payload=payload.value, | ||
| end_timestamp_precise=val["end_timestamp_precise"], | ||
| is_segment_span=bool(val.get("parent_span_id") is None or val.get("is_remote")), | ||
|
Comment on lines
+134
to
142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential
🤖 Prompt for AI Agents |
||
| ) | ||
| spans.append(span) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-sentry-mirror
Length of output: 3678
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-sentry-mirror
Length of output: 67
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-sentry-mirror
Length of output: 569
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-sentry-mirror
Length of output: 730
Add None check before accessing
organization_context.member.has_global_access.RpcUserOrganizationContext.memberis typed asRpcOrganizationMember | Noneand can beNonewhen a user does not have membership with the organization. Line 71 accesses.has_global_accesson this potentiallyNonevalue, which will raiseAttributeError.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents