chronaxis.execution.record_rule_timeline
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""用最新必要信号在线推进一条规则,不保存输入历史或第二时间轴。23只暂留一个完整条件组,等下一组或真实EOF确定其是否为末点。实际模式调度4必要TTL和规则截止;记录周期模式只在选定驱动更新时增加等待量。输出直接5交调用方结果消费者,输入/共享状态和结果仓不归本对象所有。6"""78from dataclasses import replace9from ..core.temporal import ConditionTick, TemporalRule10from ..core.types import Truth as T, QualityFlag as Q11from ..results.errors import ResultClosedError12from ..results.spool import _reserve_owned_ram, _release_owned_ram, _cleanup_owner13from .driver_expression import DriverExpression14from .temporal_buffer import BudgetedTemporalDetector15from .history import _metadata_bytes16from .record_alarm_duration import RecordAlarmDuration17from .record_delta_condition import RecordDeltaCondition181920class RecordRuleTimeline:21 """拥有规则表达式、检测器和一个待定条件组;原始组由父会话原子提交。"""2223 def __init__(self,plan,request,state,budget,step_ns,driver,observation_ends,consumer,step_consumer=None):24 """建立有限声明,不读来源,也不把请求域当作已观测范围。2526 参数: plan/request同计划规则、state共享最新值、budget共享预算、step_ns27 为None或周期,driver为所选别名,observation_ends借用EOF终点,consumer接事件。28 返回: 新owner。异常: 原构造错误附record_rule_cleanup_owner。29 """30 self._plan,self._request,self._state,self._budget=plan,request,state,budget31 self._step,self._driver,self._ends,self._consumer=step_ns,driver,observation_ends,consumer32 self._step_consumer=step_consumer33 self._held_cost=self._fixed_cost=034 self._expressions=[]35 self._detector=self._pending_owner=self._batch=None36 self._alarm=self._delta=None37 self._delta_emitted=False38 # 常量大小的业务概况,不保留条件轨迹;关闭仍由原固定预算负责。39 self._seen_true=self._seen_false=self._seen_unknown=False40 # 仅对象计数请求需要发现缺口的原因,并忽略EOF人为封口的UNKNOWN状态。41 self._count_quality = Q.NONE42 self._track_count = any(c.count_target == request.rule_id for c in plan.calculations43 if c.operation in ('point_event_count', 'activity_count')) or any(44 w.event == request.rule_id and any(c.operation == 'segment_count' and c.count_target == w.window_id45 for c in plan.calculations) for w in plan.windows)46 self._event_count=047 if plan.aggregations and not self._track_count:48 from .result_aggregations import discovery_rules49 self._track_count=any(request.rule_id in discovery_rules(plan,agg) for agg in plan.aggregations)50 self._required_aliases=set()51 self._pending=None52 self._next_expiry=self._last_driver=None53 self._start=None54 self._end=plan.dataset.domain.end_ns55 self._full_domain=not plan.dataset.domain_explicit56 self._closed=self._closing=self._failed=self._finished=False57 try:58 _reserve_owned_ram(self,131584+32*_metadata_bytes(request),'_fixed_cost')59 for expression in request.expressions:60 try:61 owner=DriverExpression(expression,state,None,backend=plan.execution.backend,62 derived=tuple(item.definition for item in plan.derived),observation_ends=observation_ends)63 except BaseException as error:64 self._pending_owner=getattr(error,'driver_expression_cleanup_owner',None)65 raise66 self._expressions.append(owner)67 if not owner._has_where:68 self._required_aliases.update(expression.dependencies)69 if request.rule_type=='window_delta':70 try:71 self._delta=RecordDeltaCondition(budget,request.window_delta,72 request.condition.root.dtype,plan.execution.backend,step_ns)73 except BaseException as error:74 self._delta=getattr(error,'record_delta_cleanup_owner',None)75 raise76 from ..planning.transforms import physical_dependencies77 names=physical_dependencies(plan,set(request.dependencies)|({driver} if driver else set()))78 self.sources=frozenset(state.definitions[name].source for name in names)79 if not self.sources:80 self.sources=frozenset(source.name for source in plan.sources)81 if request.stability is not None and request.stability.track_duration:82 try:83 self._alarm=RecordAlarmDuration(budget,step_ns,plan.execution.backend,plan.dataset.domain)84 except BaseException as error:85 self._alarm=getattr(error,'record_alarm_cleanup_owner',None)86 raise87 except BaseException as error:88 _cleanup_owner(self,error,'record_rule_cleanup_owner','在线规则构造')89 raise9091 def extend_observation(self, end_ns):92 """更新单遍读取已见的末端;最后条件组仍等待后继或EOF。参数: 实际末端。返回: None。异常: 回退。"""93 if not self._full_domain or self._finished:94 return95 self._end=end_ns96 if self._detector is not None:97 self._detector.extend_observed_end(end_ns)98 if self._alarm is not None:99 self._alarm._window=self._plan.dataset.domain100101 def _open(self):102 """拒绝失效实例。参数: 无。返回: None。异常: ResultClosedError。"""103 if self._closed or self._closing or self._failed:104 raise ResultClosedError('在线规则已关闭或失败')105106 def begin(self,observed_start):107 """用实际观察起点建立检测器,空来源由父跳过。108109 参数: observed_start实际首端。返回: None。异常: 构造/范围错误附可清理owner。110 """111 self._open()112 if self._start is not None:113 raise ValueError('在线规则不能重复开始')114 start=max(observed_start,self._plan.dataset.domain.start_ns)115 if start>self._end:116 self._finished=True117 return118 request=self._request119 rule=TemporalRule(request.rule_id,self._plan.dataset.dataset_id,self._plan.dataset.session_id,120 request.temporal_rule_type,request.minimum_duration_ns or 0,request.emit_initial,request.window_duration_ns or 0,121 duration_comparison=request.duration_comparison)122 try:123 self._detector=BudgetedTemporalDetector(self._budget,rule,start,self._end,124 backend=self._plan.execution.backend,125 stability=replace(request.stability,track_duration=False) if self._alarm is not None else request.stability,126 start_end=request.start_end,127 record_clock=self._step is not None)128 self._start=start129 except BaseException as error:130 self._detector=getattr(error,'temporal_detector_cleanup_owner',None)131 self._failed=True132 error.record_rule_cleanup_owner=self133 raise134135 def _snapshot(self,time_ns,actual,terminal=False):136 """收齐进出条件的点/右侧值,保一个必要组。参数: 实际时间/更新/EOF。返回: None。异常: 求值原错。"""137 ticks=[]138 deadlines=[]139 selected=self._state.get(self._driver) if self._driver else None140 is_driver=actual and selected is not None and selected.time_ns==time_ns141 for expression in self._expressions:142 evidence=self._required_aliases if expression._has_where else None143 point,_=expression.evaluate_at(time_ns,continuous=False,unobserved=evidence)144 right,deadline=expression.evaluate_at(time_ns,unobserved=evidence if not terminal else None)145 if self._delta is not None:146 point,right=self._delta.update(time_ns,point,right,deadline,driver=is_driver)147 deadline=self._delta.next_deadline148 point_state=T.TRUE if point.valid and point.value else T.FALSE if point.valid else T.UNKNOWN149 right_state=T.TRUE if right.valid and right.value else T.FALSE if right.valid else T.UNKNOWN150 point_quality=Q.NONE if point.valid else point.quality151 quality=Q.NONE if right.valid else right.quality152 observed_end=any(self._ends.get(source)==time_ns for source in self.sources)153 if observed_end and expression._has_where:154 observed_end=expression.active_end_at(time_ns)155 updated=self._delta is not None or (actual and expression.actual_update_at(time_ns))156 actual_unknown=updated and not point.valid157 if actual_unknown and expression._has_where and self._delta is None:158 actual_unknown=expression.actual_unknown_at(time_ns,point)159 ticks.append(ConditionTick(time_ns,T.UNKNOWN if terminal else right_state,160 quality|Q.MISSING if terminal else quality,actual_update=updated,161 observation_end=observed_end,terminal=terminal,point_state=point_state,point_quality=point_quality,162 actual_unknown=actual_unknown,diagnostic_quality=point.quality|right.quality|163 (Q.ESTIMATED if point.partial or right.partial and not terminal else Q.NONE)))164 if deadline is not None:165 deadlines.append(deadline)166 advance=None167 is_driver=False168 if self._step is not None:169 selected=self._state.get(self._driver)170 is_driver=actual and selected is not None and selected.time_ns==time_ns171 advance=self._step if is_driver and self._last_driver is not None else 0172 if is_driver:173 self._last_driver=time_ns174 self._pending=(tuple(ticks),advance,is_driver)175 self._next_expiry=min(deadlines) if deadlines else None176177 def _commit(self):178 """交付一个完整条件组和增量事件,消费失败不返回前缀。参数: 无。返回: None。异常: 原错。"""179 ticks,advance,driver=self._pending180 self._batch=self._detector.push((ticks[0],),None if len(ticks)==1 else (ticks[1],),181 advances=None if advance is None else (advance,))182 events=self._batch.events183 if self._delta is not None and self._request.retrigger=='once':184 if self._delta_emitted:185 events=()186 elif events:187 self._delta_emitted=True188 # 真实末点可以说明当时条件,但EOF人为补出的UNKNOWN不算数据缺口。189 tick=ticks[0]190 truths=(tick.point_state,) if tick.terminal else (tick.state,tick.point_state)191 self._seen_true |= T.TRUE in truths192 if self._track_count:193 for item in ticks:194 self._count_quality |= item.diagnostic_quality & Q.ESTIMATED195 for item in ticks:196 if item.point_state == T.UNKNOWN:197 self._count_quality |= item.point_quality198 if not item.terminal and item.state == T.UNKNOWN:199 self._count_quality |= item.quality200 self._seen_false |= T.FALSE in truths201 self._seen_unknown |= any(T.UNKNOWN in ((item.point_state,) if item.terminal202 else (item.state,item.point_state)) for item in ticks)203 self._event_count += len(events)204 primary=None205 try:206 if self._alarm is not None:207 state=T.UNKNOWN if ticks[0].terminal else self._detector._detector.state208 self._alarm.consume(ticks[0].time_ns,state)209 if self._step_consumer is not None:210 self._step_consumer(self._request.rule_id,ticks[0],events)211 for event in events:212 self._consumer(event)213 except BaseException as error:214 primary=error215 raise216 finally:217 event=ticks=None218 try:219 self._batch.close()220 self._batch=None221 except BaseException as cleanup:222 if primary is None:223 raise224 primary.add_note('规则输出批清理失败:'+type(cleanup).__name__)225 self._pending=None226227 def _drain_before(self,time_ns):228 """只推进已经由后续观测证明存在的区间。参数: 下个实际边界。返回: None。异常: 原推进错。"""229 if self._pending is None and self._detector._detector.last_time is None and self._start<time_ns:230 self._snapshot(self._start,False)231 while True:232 if self._pending is not None:233 if self._pending[0][0].time_ns>=time_ns:234 return235 self._commit()236 next_times=[value for value in (self._next_expiry,self._detector.next_deadline_ns) if value is not None]237 if not next_times or min(next_times)>=time_ns:238 return239 self._snapshot(min(next_times),False)240241 def before_group(self,time_ns):242 """父提交新组前处理旧状态上的TTL和实际等待。参数: 新组时间。返回: None。异常: 原错。"""243 self._open()244 if self._finished or self._start is None or time_ns<self._start:245 return246 try:247 if time_ns>self._end:248 self.finish(self._end)249 else:250 self._drain_before(time_ns)251 except BaseException as error:252 self._failed=True253 error.record_rule_cleanup_owner=self254 raise255256 def after_group(self,time_ns):257 """父原子提交后保存新条件组;实际终点在同刻更新之后结算。258259 参数: 新组时间。返回: None。异常: 原求值/事件错误,失败只能关闭。260 """261 self._open()262 if self._finished or self._start is None or time_ns<self._start:263 return264 try:265 terminal=time_ns==self._end and not self._full_domain266 self._snapshot(time_ns,True,terminal=terminal)267 if terminal:268 self._commit()269 self._batch=self._detector.finish()270 self._batch.close();self._batch=None271 self._finished=True272 except BaseException as error:273 self._failed=True274 error.record_rule_cleanup_owner=self275 raise276277 def clock_group(self,group):278 """待定旧条件已经交付后,接收报警累计所需的完整驱动对。279280 参数: 新的真实同刻组。返回: None。异常: 累计顺序/预算错误。281 不改变事件坐标,也不把请求域外样本算入报警;只借它们确定裁剪分母。282 """283 if self._alarm is not None and self._step is not None and any(s.name==self._driver for s in group.samples):284 self._alarm.driver(group.time_ns)285286 def finish(self,observed_end):287 """所有必要来源EOF后按真实末端封口,不外推到请求结束。288289 参数: observed_end必要来源联合末端。返回: None。异常: 原错,未完成不发布结果。290 """291 self._open()292 if self._finished or self._start is None:293 return294 try:295 end=min(observed_end,self._end)296 if end<self._start:297 self._finished=True298 return299 self._detector.set_observed_end(end)300 self._end=end301 self._drain_before(end)302 if self._pending is not None and self._pending[0][0].time_ns==end:303 ticks,advance,driver=self._pending304 self._pending=(tuple(replace(t,terminal=True,state=T.UNKNOWN,quality=t.quality|Q.MISSING) for t in ticks),advance,driver)305 else:306 self._snapshot(end,False,terminal=True)307 self._commit()308 self._batch=self._detector.finish()309 self._batch.close();self._batch=None310 self._finished=True311 except BaseException as error:312 self._failed=True313 error.record_rule_cleanup_owner=self314 raise315316 def outcome(self):317 """冻结规则结果原因,不读取输入。参数: 无。返回: RuleOutcome。异常: 未完成或已关闭。318319 有事件时始终保留计数;缺信号和UNKNOWN不被解释成条件为假。320 """321 self._open()322 if not self._finished and self._start is not None:323 raise ValueError('规则未结束,不能报告结果概况')324 from ..results.analysis_outcome import RuleOutcome325 # 无观测只报告实际求值需要且一直没有观测的别名。静态绑定仍校验两个分支。326 missing=tuple(name for name in self._request.dependencies if self._state.get(name) is None327 and (self._start is None or name in self._required_aliases))328 # 已产生的事件与未观测/未知事实并存;无事件时才用数据充分性解释原因。329 if self._event_count:330 status='events_found'331 elif self._start is None or missing:332 status='no_observation'333 elif self._seen_unknown:334 status='insufficient_data'335 elif self._request.rule_type=='condition_duration' and self._seen_true:336 status='duration_not_met'337 elif not self._seen_true:338 status='condition_not_met'339 else:340 status='no_event'341 return RuleOutcome(self._request.rule_id,status,self._event_count,self._seen_unknown,missing)342343 def count_evidence(self, domain):344 """判定规则实际观测是否覆盖整个目标会话,保留发现是否完整的证据。345346 参数: domain为最终会话域。返回: 已观测、至少部分可判定、完整、质量位。347 异常: 规则尚未结束或已关闭时沿outcome检查传播。348 """349 outcome = self.outcome()350 observed = self._start is not None and outcome.status != 'no_observation'351 known = self._seen_true or self._seen_false352 # 已确认的末点可以产生点事件;显式请求域超出来源实际首末时仍有缺口。353 covers_request = (observed and domain is not None354 and self._start <= domain.start_ns and self._end >= domain.end_ns)355 complete = covers_request and not self._seen_unknown and not outcome.unobserved_signals and not self._count_quality & Q.ESTIMATED356 quality = self._count_quality357 if not covers_request or outcome.unobserved_signals:358 quality |= Q.MISSING359 return observed, known, complete, quality360361 @property362 def duration_result(self):363 """借出明确请求的最终累计;空来源返回无观测。参数: 无。返回: tuple或None。异常: 未完成错误。"""364 if self._alarm is None:365 return None366 if not self._finished and self._start is not None:367 raise ValueError('报警尚未完成')368 return self._alarm.finish()369370 @property371 def closed(self):372 """返回资源关闭状态。参数: 无。返回: bool。异常: 无。"""373 return self._closed374375 def close(self):376 """释放自有批、表达式和检测器,保留失败槽供重试。参数: 无。返回: None。异常: 原关闭错。"""377 if self._closed:378 return379 self._closing=True380 for slot in ('_batch','_pending_owner','_detector','_alarm','_delta'):381 owner=getattr(self,slot)382 if owner is not None:383 owner.close()384 setattr(self,slot,None)385 for index,owner in enumerate(self._expressions):386 if owner is not None:387 owner.close();self._expressions[index]=None388 self._pending=self._consumer=self._step_consumer=self._state=self._plan=self._ends=self._required_aliases=None389 _release_owned_ram(self,self._fixed_cost,'_fixed_cost')390 self._closed=True391