fix(audit): 修复 _collect_snapshot 关系守卫失效导致的审计丢失
hasattr(attr, 'property') 对 inspect(obj).attrs 产出的 AttributeState
恒为 False,这个守卫从未生效过 —— 关系属性一律被 getattr() 取了值。
lazy='dynamic' 的关系(如 SysWarehouseLocation.children)拿到的是
AppenderQuery,_serialize_value 对非基础类型原样放行,最终 json.dumps
兜底走 str(query) → query.session → Session.flush(),而此刻正在 flush 中,
抛 InvalidRequestError("Session is already flushing"),整条审计被 except 吞掉。
后果:
- sys_warehouse_location 的新建/删除审计长期为 0 条
(UPDATE 走 attr.history 而非 getattr,所以不受影响,症状是间歇性的)
- 批量生成 3050 个节点时逐行抛 3050 次异常:请求 873ms → 修复后 349ms
改为按 inspect(cls).relationships 的名字排除;before_update_listener
里的同一处坏守卫一并修正。
另把 warehouse 登记进 AGGREGATE_PATH_MARKERS:库位批量生成一次可产出
3000+ 节点,未登记时一次点击写 3000+ 条审计,登记后只留一条带 targets
清单的记录。
This commit is contained in:
@ -149,6 +149,10 @@ AGGREGATE_PATH_MARKERS = (
|
||||
'inbound/stock/stocktake/generate-missing',
|
||||
'outbound',
|
||||
'import/execute',
|
||||
# 库位管理:批量生成一次可产出 3000+ 个节点,批量删除同理。
|
||||
# 未登记时一次点击就写 3000+ 条审计(且叠加 _collect_snapshot 旧守卫的逐行异常),
|
||||
# 登记后一次点击只留一条带 targets 清单的记录。
|
||||
'warehouse',
|
||||
)
|
||||
|
||||
# 聚合日志里保留的受影响对象清单上限。
|
||||
@ -571,12 +575,21 @@ def _collect_snapshot(target):
|
||||
"""收集对象全字段快照(跳过忽略字段与关系属性)"""
|
||||
snap = {}
|
||||
state = inspect(target)
|
||||
# ★ 关系属性必须按**名字**排除。原守卫写的是
|
||||
# hasattr(attr, 'property') and hasattr(attr.property, 'direction')
|
||||
# 而 state.attrs 产出的是 AttributeState —— 它没有 .property,守卫恒为 False,
|
||||
# 关系属性从来没被跳过过。后果不只是"快照里多几个字段":
|
||||
# lazy='dynamic' 的关系(如 SysWarehouseLocation.children)取值得到的是
|
||||
# AppenderQuery,_serialize_value 对非基础类型原样放行,最终 json.dumps
|
||||
# 兜底走 str(query) → query.session → Session.flush(),而此刻正在 flush 中
|
||||
# → InvalidRequestError("Session is already flushing") → 整条审计被 except 吞掉。
|
||||
# 实测库位的新建/删除审计因此长期为 0 条,批量生成 3000 个节点会逐行抛 3000 次。
|
||||
rel_keys = set(inspect(target.__class__).relationships.keys())
|
||||
for attr in state.attrs:
|
||||
key = attr.key
|
||||
if _should_skip_field(key):
|
||||
if key in rel_keys:
|
||||
continue
|
||||
# 跳过关系属性(如 .base / .material),它们不是列
|
||||
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
|
||||
if _should_skip_field(key):
|
||||
continue
|
||||
snap[key] = _serialize_value(getattr(target, key, None))
|
||||
return snap
|
||||
@ -604,11 +617,15 @@ def before_update_listener(mapper, connection, target):
|
||||
try:
|
||||
state = inspect(target)
|
||||
changes = {}
|
||||
# 同 _collect_snapshot:关系属性按名字排除(原守卫基于不存在的 .property,恒为 False)。
|
||||
# 这里尤其危险 —— 关系一旦有 history,新旧值会被原样放进 changes,
|
||||
# 后续 json.dumps 兜底 str() 到 AppenderQuery 就是同一个 flush 崩溃。
|
||||
rel_keys = set(inspect(target.__class__).relationships.keys())
|
||||
for attr in state.attrs:
|
||||
key = attr.key
|
||||
if _should_skip_field(key):
|
||||
if key in rel_keys:
|
||||
continue
|
||||
if hasattr(attr, 'property') and hasattr(attr.property, 'direction'):
|
||||
if _should_skip_field(key):
|
||||
continue
|
||||
if not attr.history.has_changes():
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user