-
Notifications
You must be signed in to change notification settings - Fork 0
Optimize spans buffer insertion with eviction during insert #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: performance-optimization-baseline
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -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]) | ||
|
|
||
| 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 | ||
|
|
||
| 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]) | ||
|
|
||
| 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] | ||
|
|
||
| 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 | ||
|
Comment on lines
+821
to
+911
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 chainOptimizedCursorPaginator relies on negative queryset slicing and duplicates BasePaginator logic This new paginator is where negative offsets will actually be exercised ( On top of that, Concrete suggestions:
Until you have a proven-safe implementation strategy, consider failing fast instead of slicing with negative indices: 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])
+ if self.enable_advanced_features and offset < 0:
+ raise BadPaginationError("Pagination offset cannot be negative for optimized cursors")
+
+ start_offset = max(0, offset) if not cursor.is_prev else offset
+ stop = start_offset + limit + extra
+ results = list(queryset[start_offset:stop])This preserves current, well‑understood behavior while preventing advanced mode from silently relying on unsupported queryset semantics.
Given how similar this implementation is to
That will make it much easier to keep behavior aligned between the standard and optimized paginators as future changes land.
Whatever final behavior you decide (reject, clamp, or alternate strategy), unit tests that exercise:
will be important to avoid regressions in these high‑traffic endpoints. 🌐 Web query: 💡 Result: Short answer
Recommended patterns for efficient reverse (cursor / keyset) pagination
Minimal implementation sketch (concept)
References
Length of output: 4842 🏁 Script executed: #!/bin/bash
# Check if BadPaginationError is already defined in the codebase
rg -n "BadPaginationError" --type=pyLength of output: 1500 🏁 Script executed: #!/bin/bash
# Look for exception classes in the paginator module
rg -n "class.*Error" src/sentry/api/paginator.pyLength of output: 83 🏁 Script executed: #!/bin/bash
# Check BasePaginator.get_result to see the duplication
ast-grep --pattern 'class BasePaginator {
$$$
def get_result($_, $_) {
$$$
}
$$$
}'Length of output: 0 🏁 Script executed: #!/bin/bash
# Search for tests related to OptimizedCursorPaginator or negative offsets
fd test.*paginator -x cat {} 2>/dev/null | grep -A 20 -B 5 "OptimizedCursorPaginator\|negative.*offset"Length of output: 894 🏁 Script executed: #!/bin/bash
# Look for imports and exception definitions in api module
fd __init__.py src/sentry/api/ -x grep -l "Error\|Exception" {}Length of output: 105 Length of output: 50 🏁 Script executed: #!/bin/bash
# Get the actual line count of paginator.py to check if 821-911 is valid
wc -l src/sentry/api/paginator.pyLength of output: 31 🏁 Script executed: #!/bin/bash
# Check the full content from line 800 onwards to see what's actually there
sed -n '800,950p' src/sentry/api/paginator.pyLength of output: 4208 🏁 Script executed: #!/bin/bash
# Search for any reference to "enable_advanced_features" which is a key marker in the review comment
rg -n "enable_advanced_features" src/sentry/api/paginator.pyLength of output: 213 🏁 Script executed: #!/bin/bash
# Look for BasePaginator class and its get_result method
rg -n "class BasePaginator" src/sentry/api/paginator.pyLength of output: 23 🏁 Script executed: #!/bin/bash
# Extract BasePaginator.get_result method to compare
sed -n '200,450p' src/sentry/api/paginator.py | head -100Length of output: 3309 Length of output: 319 🏁 Script executed: #!/bin/bash
# Extract full BasePaginator.get_result method to compare duplication
sed -n '150,220p' src/sentry/api/paginator.pyLength of output: 2939 🏁 Script executed: #!/bin/bash
# Verify line count and structure of both get_result implementations
rg -n "def get_result" src/sentry/api/paginator.pyLength of output: 615 🏁 Script executed: #!/bin/bash
# Check if there are any comment notes about the advanced features flag
rg -n "advanced_features\|OPTIMIZED\|optimized_pagination" src/sentry/api/paginator.pyLength of output: 0 🏁 Script executed: #!/bin/bash
# Search for imports of math module (used in get_item_key) to verify both classes have the same capability
rg -n "^import math" src/sentry/api/paginator.pyLength of output: 13 Guard OptimizedCursorPaginator against negative offsets; eliminate code duplication; add test coverage This new paginator is already deployed in Additionally, Required fixes:
offset = cursor.offset
extra = 1
if cursor.is_prev and cursor.value:
extra += 1
- if self.enable_advanced_features and cursor.offset < 0:
- start_offset = cursor.offset
- 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])
+ if self.enable_advanced_features and offset < 0:
+ raise BadPaginationError("Pagination offset cannot be negative")
+
+ start_offset = max(0, offset) if not cursor.is_prev else offset
+ stop = start_offset + limit + extra
+ results = list(queryset[start_offset:stop])
🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ local main_redirect_key = string.format("span-buf:sr:{%s}", project_and_trace) | |
| local set_span_id = parent_span_id | ||
| local redirect_depth = 0 | ||
|
|
||
| for i = 0, 10000 do -- theoretically this limit means that segment trees of depth 10k may not be joined together correctly. | ||
| for i = 0, 1000 do | ||
| local new_set_span = redis.call("hget", main_redirect_key, set_span_id) | ||
| redirect_depth = i | ||
| if not new_set_span or new_set_span == set_span_id then | ||
|
|
@@ -40,19 +40,29 @@ end | |
| redis.call("hset", main_redirect_key, span_id, set_span_id) | ||
| redis.call("expire", main_redirect_key, set_timeout) | ||
|
|
||
| local span_count = 0 | ||
|
|
||
| local set_key = string.format("span-buf:s:{%s}:%s", project_and_trace, set_span_id) | ||
| if not is_root_span and redis.call("scard", span_key) > 0 then | ||
| redis.call("sunionstore", set_key, set_key, span_key) | ||
| if not is_root_span and redis.call("zcard", span_key) > 0 then | ||
| span_count = redis.call("zunionstore", set_key, 2, set_key, span_key) | ||
| redis.call("unlink", span_key) | ||
| end | ||
|
|
||
| local parent_key = string.format("span-buf:s:{%s}:%s", project_and_trace, parent_span_id) | ||
| if set_span_id ~= parent_span_id and redis.call("scard", parent_key) > 0 then | ||
| redis.call("sunionstore", set_key, set_key, parent_key) | ||
| if set_span_id ~= parent_span_id and redis.call("zcard", parent_key) > 0 then | ||
| span_count = redis.call("zunionstore", set_key, 2, set_key, parent_key) | ||
| redis.call("unlink", parent_key) | ||
| end | ||
| redis.call("expire", set_key, set_timeout) | ||
|
|
||
| if span_count == 0 then | ||
| span_count = redis.call("zcard", set_key) | ||
| end | ||
|
|
||
| if span_count > 1000 then | ||
| redis.call("zpopmin", set_key, span_count - 1000) | ||
| end | ||
|
Comment on lines
+43
to
+64
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 chainEviction strategy removes oldest spans - verify this is correct behavior. The new eviction logic uses
Additionally, the sorted set operations ( Verify the eviction behavior is acceptable: Consider these alternatives:
-- Alternative: Fail fast instead of silent eviction
if span_count > 1000 then
redis.log(redis.LOG_WARNING, "Segment exceeded 1000 spans: " .. set_key)
-- Optionally: return error instead of silently dropping
end🏁 Script executed: #!/bin/bash
# Check if any traces in production exceed 1000 spans
rg -n -C3 'max_segment_spans'
# Search for monitoring or alerts on segment size
rg -n 'segment.*span.*count|num_spans_per_segment'Length of output: 94793 The web search didn't provide the specific implementation details. Let me search the codebase directly for how spans are added to the sorted set. [/run_scripts] 🏁 Script executed: #!/bin/bash
# Look for how sorted set is built and what score is used for spans
rg -n -A5 -B5 "ZADD\|zadd\|zunionstore" src/sentry/spans/buffer.pyLength of output: 0 Length of output: 0 Length of output: 0 Length of output: 7727 Length of output: 0 Length of output: 3991 Length of output: 2098 Hardcoded eviction threshold in Lua ignores Python configuration parameter—causing silent data loss without monitoring. The review's concern is partially correct but incomplete. The code has multiple issues:
Issues requiring fixes:
|
||
|
|
||
| local has_root_span_key = string.format("span-buf:hrs:%s", set_key) | ||
| local has_root_span = redis.call("get", has_root_span_key) == "1" or is_root_span | ||
| if has_root_span then | ||
|
|
||
| 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
143
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 chainVerify that Line 141 accesses Run the following script to check if there are any payload producers or test cases that might send spans without this field: If any producers don't guarantee this field, consider adding error handling: val = cast(SpanEvent, rapidjson.loads(payload.value))
+ end_timestamp = val.get("end_timestamp_precise")
+ if end_timestamp is None:
+ # Log error or use fallback
+ end_timestamp = timestamp # or raise exception
+
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"],
+ end_timestamp_precise=end_timestamp,
is_segment_span=bool(val.get("parent_span_id") is None or val.get("is_remote")),
)🏁 Script executed: #!/bin/bash
# Search for span payload construction that might be missing end_timestamp_precise
rg -n -C3 -g '!*.pyc' '"span_id".*"trace_id"' | rg -v 'end_timestamp_precise'Length of output: 1355 I need to investigate further to understand if I need to check the Span dataclass definition and how 🏁 Script executed: #!/bin/bash
# Find the Span class definition
ast-grep --pattern $'class Span($_) {
$$$
}'Length of output: 0 🏁 Script executed: #!/bin/bash
# Search for end_timestamp_precise usage and definition
rg -n 'end_timestamp_precise' -B2 -A2Length of output: 43239 🏁 Script executed: #!/bin/bash
# Look at the specific file to understand context better
cat src/sentry/spans/consumers/process/factory.py | head -200Length of output: 5354 I need to check the SpanEvent schema definition and any validation logic before this code runs. Length of output: 0 🏁 Script executed: #!/bin/bash
# Search for validation or normalization of span payload
rg -n 'process_batch|rapidjson.loads' -B 5 -A 5 src/sentry/spans/consumers/process/factory.pyLength of output: 1551 🏁 Script executed: #!/bin/bash
# Check if end_timestamp_precise might be optional in the schema
cat src/sentry/issues/event.schema.json | grep -A 30 '"span_id".*"trace_id"' | head -60Length of output: 1423 Length of output: 2996 🏁 Script executed: #!/bin/bash
# Check if there's any normalization or validation happening on payloads
rg -n 'end_timestamp_precise' src/sentry/spans/consumers/process/ -B 5Length of output: 568 🏁 Script executed: #!/bin/bash
# Look for exception handling in related code
rg -n 'KeyError|try:|except' src/sentry/spans/consumers/process/factory.pyLength of output: 0 Length of output: 1087 🏁 Script executed: #!/bin/bash
# Search for any documentation or comments about span payload requirements
rg -n 'end_timestamp_precise|end_timestamp' tests/sentry/spans/consumers/process/test_consumer.py -B 3 -A 1Length of output: 501 🏁 Script executed: #!/bin/bash
# Check if there are any recent changes or comments about field handling
git log --oneline -20 src/sentry/spans/consumers/process/factory.py 2>/dev/null || echo "Git not available"Length of output: 168 Length of output: 494 🏁 Script executed: #!/bin/bash
# Look at Span dataclass - are all accessed fields actually required?
cat src/sentry/spans/buffer.py | grep -A 10 "^class Span"Length of output: 669 🏁 Script executed: #!/bin/bash
# Check how other "required" fields are accessed - is there a pattern?
rg -n 'val\["trace_id"|val\["project_id"|val\["span_id"' src/sentry/spans/consumers/process/factory.pyLength of output: 127 Length of output: 0 🏁 Script executed: #!/bin/bash
# Check if there's error handling at a higher level (in RunTask or strategy)
rg -n 'except|KeyError' src/sentry/spans/consumers/process/factory.pyLength of output: 0 🌐 Web query: 💡 Result: Short answer — per Sentry's Span Protocol (used by ingest pipelines / kafka schemas), a Span (SpanEvent) must include at minimum:
(Other fields often present: parent_span_id (nullable), status, kind, is_remote, attributes, and events — but those are optional depending on SDK/exporter.) Source: Sentry Span Protocol documentation. [1][2] Sources: Add defensive handling for Per the Span Protocol, required span fields include trace_id, span_id, name, start_timestamp, and end_timestamp, but val = cast(SpanEvent, rapidjson.loads(payload.value))
+ end_timestamp = val.get("end_timestamp_precise")
+ if end_timestamp is None:
+ logger.error("Span missing end_timestamp_precise")
+ end_timestamp = val.get("end_timestamp", timestamp)
+
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"],
+ end_timestamp_precise=end_timestamp,
is_segment_span=bool(val.get("parent_span_id") is None or val.get("is_remote")),
)
🤖 Prompt for AI Agents |
||
| spans.append(span) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,8 @@ def __init__( | |||||||||
| has_results: bool | None = None, | ||||||||||
| ): | ||||||||||
| self.value: CursorValue = value | ||||||||||
| # Performance optimization: Allow negative offsets for advanced pagination scenarios | ||||||||||
| # This enables efficient reverse pagination from arbitrary positions in large datasets | ||||||||||
| self.offset = int(offset) | ||||||||||
|
Comment on lines
+26
to
28
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. Misleading documentation: No code change supports negative offsets. The comment claims to "allow negative offsets for advanced pagination scenarios," but the code still simply casts offset to Consider either:
- # Performance optimization: Allow negative offsets for advanced pagination scenarios
- # This enables efficient reverse pagination from arbitrary positions in large datasets
self.offset = int(offset)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| self.is_prev = bool(is_prev) | ||||||||||
| self.has_results = has_results | ||||||||||
|
|
||||||||||
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
Negative offset assumption here likely breaks with Django QuerySet slicing
The new comment and logic assume that negative offsets are safe because the queryset will “handle boundary conditions.” However, Django
QuerySetslicing does not support negativestart/stopindices – attemptingqueryset[-N:M]raises an assertion rather than clamping. If a cursor with a negativeoffsetever reaches this code path (which seems to be the intent for reverse pagination), this will likely result in a 500 rather than a graceful boundary behavior.Additionally, the rest of
get_result(thelen(results) == offset + limit + extracheck) still reasons aboutoffsetas if it were non‑negative, so even if negative slicing worked, the edge‑case handling would be inconsistent for negative offsets.I’d strongly recommend not relying on negative queryset slice indices here. For now, a safer approach is to either:
or
Once a robust strategy for reverse pagination is designed (likely not via negative slice indices), you can reintroduce the optimization with tests that exercise negative offsets end‑to‑end.
🌐 Web query:
💡 Result:
Short answer: Negative start/stop indices are not supported — Django raises an error. The docs state "Negative indexing ... is not supported," and the QuerySet getitem implementation raises a ValueError for negative indices. [1][2]
Sources:
Now let me examine the actual code in the repository to verify if this is an issue that can actually occur:
[/scratchpad_end] -->
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 1534
🏁 Script executed:
Length of output: 5867
Critical issue confirmed: Django QuerySet does not support negative slice indices and will raise ValueError
The code review comment is correct. Django raises an error for negative indices, and the QuerySet getitem implementation raises a ValueError for negative indices.
The problematic code paths are confirmed:
BasePaginator line 182: When
cursor.is_previs True,start_offset = offsetis passed directly without clamping. Ifoffsetis negative, this triggers Django's ValueError.OptimizedCursorPaginator lines 880-885: The code explicitly allows negative offsets when
enable_advanced_features=Trueviaif self.enable_advanced_features and cursor.offset < 0:, passing them directly toqueryset[start_offset:stop]. The comment at line 876 claiming "The underlying Django ORM properly handles negative slicing automatically" is false and contradicts Django's documented behavior.Both code paths will crash at runtime with
ValueError("Negative indexing is not supported.")rather than providing the intended "boundary condition handling."The comment in BasePaginator (lines 179-181) stating "This is safe because the underlying queryset will handle boundary conditions" is incorrect and should be fixed immediately. The OptimizedCursorPaginator's advanced feature explicitly enabling negative offsets requires either:
🤖 Prompt for AI Agents