Optimize spans buffer insertion with eviction during insert - #1
Optimize spans buffer insertion with eviction during insert#1linxia0415 wants to merge 2 commits into
Conversation
A proof of concept that limits the number of spans per segment during insertion. Internally, this uses a sorted set scored by the spans' end timestamps and evicts the oldest spans. This ensures that spans higher up in the hierarchy and more recent spans are prioritized during the eviction.
…loyments This change introduces optimized cursor-based pagination for audit log endpoints to improve performance in enterprise environments with large audit datasets. Key improvements: - Added OptimizedCursorPaginator with advanced boundary handling - Enhanced cursor offset support for efficient bi-directional navigation - Performance optimizations for administrative audit log access patterns - Backward compatible with existing DateTimePaginator implementation The enhanced paginator enables more efficient traversal of large audit datasets while maintaining security boundaries and access controls. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
AI代码审查报告
变更概览
本次 PR 涉及 9 个文件,新增 +183 行,删除 -29 行。
功能变更摘要
本 PR 为审计日志和 span 数据添加基于时间的分页支持。引入了新的时间游标分页器,用于按时间范围高效检索数据。改造了 span 缓冲和消费流程以集成时间戳游标,并补充了相应的单元测试验证新分页逻辑的正确性。
文件变更摘要
| 文件 | 变更 | 行数 | 摘要 | 发现问题 |
|---|---|---|---|---|
src/sentry/api/paginator.py |
修改 | +101/-2 | 新增支持时间游标的分页器实现 | 3 个 |
src/sentry/api/endpoints/organization_auditlogs.py |
修改 | +25/-8 | 为审计日志添加时间范围分页功能 | 1 个 |
src/sentry/scripts/spans/add-buffer.lua |
修改 | +15/-5 | 优化 span 缓冲 Lua 脚本支持时间戳 | 1 个 |
tests/sentry/spans/test_buffer.py |
修改 | +22/-0 | 添加时间游标缓冲功能的测试用例 | — |
src/sentry/spans/buffer.py |
修改 | +8/-13 | 改用时间游标进行 span 缓冲分页 | — |
src/sentry/spans/consumers/process/factory.py |
修改 | +4/-1 | 集成时间游标到 span 消费流程 | — |
tests/sentry/spans/consumers/process/test_flusher.py |
修改 | +4/-0 | 验证时间游标在刷新逻辑中的行为 | — |
src/sentry/utils/cursors.py |
修改 | +2/-0 | 新增时间戳游标工具类 | — |
tests/sentry/spans/consumers/process/test_consumer.py |
修改 | +2/-0 | 补充时间游标相关测试断言 | — |
问题严重级别分布
| 级别 | 数量 | 占比 |
|---|---|---|
| 🟡 中危 | 5 | 100% |
代表性问题(至多 10 条,按严重级别优先)
- 🟡 中危
src/sentry/api/endpoints/organization_auditlogs.pyL69: 潜在 None 访问风险
line 69 处直接访问 organization_context.member.has_global_access,但未检查 member 是否为 None。当用户不是…
2. 🟡 中危 src/sentry/api/paginator.py L179: 注释与实际代码行为不符
line 179-181 的注释声称 "allow negative offsets"(允许负偏移),但实际代码 line 182 处 start_offset = max… 3. 🟡 **中危** src/sentry/api/paginator.pyL863: 分支对称性:负偏移处理缺少cursor.is_prev` 状态检查
line 863 处启用高级负偏移功能的条件仅检查 cursor.offset < 0,未检查 cursor.is_pr… 4. 🟡 **中危** src/sentry/api/paginator.py` L877: 延迟错误追踪:负偏移条件下的错误长度比较
line 863-868 处当 enable_advanced_features=True 且 cursor.offset < 0 时,允许负偏移进…
5. 🟡 中危 src/sentry/scripts/spans/add-buffer.lua L30: 循环边界错误与重定向链深度未检查
line 30 处 for i = 0, 1000 do 实际执行 1001 次迭代(0 到 1000 包含两端),超出预期的 1000 次限制。更重要的是,l…
由 CodeHawk 提供支持 · nuwa
| on_results=lambda x: serialize(x, request.user), | ||
| ) | ||
| # Performance optimization for high-volume audit log access patterns | ||
| # Enable advanced pagination features for authorized administrators |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
潜在 None 访问风险
line 69 处直接访问 organization_context.member.has_global_access,但未检查 member 是否为 None。当用户不是组织成员时,organization_context.member 可能为 None,此时访问 has_global_access 属性将触发 AttributeError,导致请求处理失败。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 69
💬 详细说明:
- 潜在 None 访问风险
- line 69 处直接访问 organization_context.member.has_global_access,但未检查 member 是否为 None。当用户不是组织成员时,organization_context.member 可能为 None,此时访问 has_global_access 属性将触发 AttributeError,导致请求处理失败。
📝 问题代码:
enable_advanced = request.user.is_superuser or organization_context.member.has_global_access
💡 修复建议:
在访问 has_global_access 属性前添加 None 检查。使用显式判空确保 member 不为 None 时才访问其属性,避免 AttributeError。
✅ 修复示例:
enable_advanced = request.user.is_superuser or (organization_context.member is not None and organization_context.member.has_global_access)
🔗 参考链接
无
|
|
||
| stop = offset + limit + extra | ||
| results = list(queryset[offset:stop]) | ||
| # Performance optimization: For high-traffic scenarios, allow negative offsets |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
注释与实际代码行为不符
line 179-181 的注释声称 "allow negative offsets"(允许负偏移),但实际代码 line 182 处 start_offset = max(0, offset) if not cursor.is_prev else offset 在 not cursor.is_prev 时明确将负偏移限制为 0。
这导致:
- 开发者可能被误导认为 BasePaginator 支持负偏移
- 实际负偏移功能被移至 OptimizedCursorPaginator 且需显式启用
enable_advanced_features - 注释与实现的不一致增加了代码维护难度
注:此问题虽较轻,但结合 OptimizedCursorPaginator 中负偏移的实现问题(line 877),表明负偏移功能的整体设计和文档需要澄清。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 179
💬 详细说明:
- 注释与实际代码行为不符
- line 179-181 的注释声称 "allow negative offsets"(允许负偏移),但实际代码 line 182 处
start_offset = max(0, offset) if not cursor.is_prev else offset在not cursor.is_prev时明确将负偏移限制为 0。 - 这导致:
-
- 开发者可能被误导认为 BasePaginator 支持负偏移
-
- 实际负偏移功能被移至 OptimizedCursorPaginator 且需显式启用
enable_advanced_features
- 实际负偏移功能被移至 OptimizedCursorPaginator 且需显式启用
-
- 注释与实现的不一致增加了代码维护难度
- 注:此问题虽较轻,但结合 OptimizedCursorPaginator 中负偏移的实现问题(line 877),表明负偏移功能的整体设计和文档需要澄清。
📝 问题代码:
# Performance optimization: For high-traffic scenarios, allow negative offsets
💡 修复建议:
修复策略:更新注释以准确描述代码行为。
操作步骤:
- 将 line 179-181 的注释改为准确描述:"Ensure non-negative offset for forward pagination to prevent unexpected slicing behavior. Negative offsets for reverse pagination are handled separately."
- 或在 OptimizedCursorPaginator 的文档字符串中明确说明负偏移功能已从 BasePaginator 迁移至此,并需要显式启用
✅ 修复示例:
# Performance optimization: Ensure non-negative offset for forward pagination
# to prevent unexpected slicing behavior. Negative offsets for reverse pagination
# (cursor.is_prev=True) are preserved. Advanced negative offset features are
# available in OptimizedCursorPaginator with enable_advanced_features=True.
start_offset = max(0, offset) if not cursor.is_prev else offset
🔗 参考链接
无
| max_hits = MAX_HITS_LIMIT | ||
| if count_hits: | ||
| hits = self.count_hits(max_hits) | ||
| elif known_hits is not None: |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
分支对称性:负偏移处理缺少
cursor.is_prev状态检查
line 863 处启用高级负偏移功能的条件仅检查 cursor.offset < 0,未检查 cursor.is_prev 状态。对比 BasePaginator(line 182)的实现,not cursor.is_prev 时偏移被限制为 max(0, offset),而 cursor.is_prev 时允许负偏移。
OptimizedCursorPaginator 的负偏移逻辑(line 863-868)未区分 is_prev 状态,导致:
- 当
cursor.is_prev=False但cursor.offset < 0时,仍会进入负偏移分支 - 这与 BasePaginator 的行为不一致,可能导致非 prev 模式下的意外数据范围查询
- 下游 line 874 的
if cursor.is_prev and cursor.value:处理逻辑与负偏移分支的交互可能产生未预期的代码路径
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 863
💬 详细说明:
- 分支对称性:负偏移处理缺少
cursor.is_prev状态检查 - line 863 处启用高级负偏移功能的条件仅检查
cursor.offset < 0,未检查cursor.is_prev状态。对比 BasePaginator(line 182)的实现,not cursor.is_prev时偏移被限制为max(0, offset),而cursor.is_prev时允许负偏移。 - OptimizedCursorPaginator 的负偏移逻辑(line 863-868)未区分
is_prev状态,导致: -
- 当
cursor.is_prev=False但cursor.offset < 0时,仍会进入负偏移分支
- 当
-
- 这与 BasePaginator 的行为不一致,可能导致非 prev 模式下的意外数据范围查询
-
- 下游 line 874 的
if cursor.is_prev and cursor.value:处理逻辑与负偏移分支的交互可能产生未预期的代码路径
- 下游 line 874 的
📝 问题代码:
if self.enable_advanced_features and cursor.offset < 0:
💡 修复建议:
修复策略:在负偏移条件中增加
cursor.is_prev检查,确保与 BasePaginator 的行为一致。
操作步骤:
- 将 line 863 的条件改为
if self.enable_advanced_features and cursor.is_prev and cursor.offset < 0: - 或者如果业务上确实需要在非 prev 模式下支持负偏移,应在注释中明确说明此设计意图,并确保 line 870-872 的 else 分支中
not cursor.is_prev时的max(0, offset)逻辑与高级功能的需求兼容
✅ 修复示例:
# 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.is_prev and cursor.offset < 0:
🔗 参考链接
无
| # 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: |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
延迟错误追踪:负偏移条件下的错误长度比较
line 863-868 处当 enable_advanced_features=True 且 cursor.offset < 0 时,允许负偏移进行高级分页。此时 start_offset = cursor.offset(负数),stop = start_offset + limit + extra。但 line 877 处使用 len(results) == offset + limit + extra 判断是否多取了元素。
当 offset 为负数时(如 -100),offset + limit + extra 可能为很小的正数、零甚至负数,而 len(results) 始终为非负整数。这导致:
- 若
offset + limit + extra < 0,条件恒为 False,无法正确裁剪多余元素 - 若
offset + limit + extra >= 0,条件判断逻辑与预期不符(原逻辑假设 offset 为非负起始位置)
结果:负偏移分页时可能返回错误数量的结果,或无法正确识别分页边界,导致数据重复或遗漏。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 877
💬 详细说明:
- 延迟错误追踪:负偏移条件下的错误长度比较
- line 863-868 处当
enable_advanced_features=True且cursor.offset < 0时,允许负偏移进行高级分页。此时start_offset = cursor.offset(负数),stop = start_offset + limit + extra。但 line 877 处使用len(results) == offset + limit + extra判断是否多取了元素。 - 当
offset为负数时(如 -100),offset + limit + extra可能为很小的正数、零甚至负数,而len(results)始终为非负整数。这导致: -
- 若
offset + limit + extra < 0,条件恒为 False,无法正确裁剪多余元素
- 若
-
- 若
offset + limit + extra >= 0,条件判断逻辑与预期不符(原逻辑假设 offset 为非负起始位置)
- 若
- 结果:负偏移分页时可能返回错误数量的结果,或无法正确识别分页边界,导致数据重复或遗漏。
📝 问题代码:
elif len(results) == offset + limit + extra:
💡 修复建议:
修复策略:使用
start_offset替代offset进行长度比较,确保与切片逻辑一致。
操作步骤:
- 将 line 877 的
offset + limit + extra改为start_offset + limit + extra - 或者更安全的做法:使用
stop - start_offset计算预期长度,即len(results) == limit + extra - 同步检查 line 192 的 BasePaginator 是否有相同问题(
len(results) == offset + limit + extra在cursor.is_prev且offset为负时同样存在问题)
✅ 修复示例:
if cursor.is_prev and cursor.value:
# If the first result is equal to the cursor_value then it's safe to filter
# it out, since the value hasn't been updated
if results and self.get_item_key(results[0], for_prev=True) == cursor.value:
results = results[1:]
# Otherwise we may have fetched an extra row, just drop it off the end if so.
elif len(results) == start_offset + limit + extra:
results = results[:-1]
🔗 参考链接
无
| 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 |
There was a problem hiding this comment.
🟡 AI 代码审查发现问题
📋 问题概述
循环边界错误与重定向链深度未检查
line 30 处 for i = 0, 1000 do 实际执行 1001 次迭代(0 到 1000 包含两端),超出预期的 1000 次限制。更重要的是,line 33-35 的 break 条件仅在找到终止节点时触发,若重定向链深度超过 1000 层,循环会因达到上限而静默退出,而非报错。此时 line 37 的 set_span_id 指向链中第 1001 个中间节点(非最终根节点),line 40 将建立错误的重定向映射(span_id → 中间节点),导致后续 line 46-55 的合并操作将数据分散到错误的 key,造成数据不一致且难以追踪。
触发条件:当某个 trace 的 span 重定向链深度 > 1000 层时(如异常深的嵌套调用或循环重定向配置错误)。
📍 问题详情
🟡 问题 1 | 严重程度: MEDIUM | 行号: 30
💬 详细说明:
- 循环边界错误与重定向链深度未检查
- line 30 处
for i = 0, 1000 do实际执行 1001 次迭代(0 到 1000 包含两端),超出预期的 1000 次限制。更重要的是,line 33-35 的 break 条件仅在找到终止节点时触发,若重定向链深度超过 1000 层,循环会因达到上限而静默退出,而非报错。此时 line 37 的set_span_id指向链中第 1001 个中间节点(非最终根节点),line 40 将建立错误的重定向映射(span_id → 中间节点),导致后续 line 46-55 的合并操作将数据分散到错误的 key,造成数据不一致且难以追踪。 - 触发条件:当某个 trace 的 span 重定向链深度 > 1000 层时(如异常深的嵌套调用或循环重定向配置错误)。
📝 问题代码:
for i = 0, 1000 do
💡 修复建议:
修正循环边界为 1000 次(
for i = 0, 999 do或for i = 1, 1000 do),并在循环后检查是否因达到上限而退出。若未找到终止节点,应返回错误回滚操作,避免数据不一致。
✅ 修复示例:
local found = false
for i = 0, 999 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
found = true
break
end
set_span_id = new_set_span
end
if not found then
return redis.error_reply("redirect chain exceeds maximum depth of 1000")
end
🔗 参考链接
无
Test 2