Chronaxis用户手册English
本页目录

chronaxis.legacy.batch_read

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在共享预算内按批取得BLF解码结果,按配置保留观测时间与所需原始记录。23复用逐条read的INIT/READY验证、实际进程关闭和失败恢复;子进程使用共用BLF读取器。4父在授予一批许可前预付两端缓冲、解析模型和时间数组的保守费用;同时只一批在途。5每批最多4096原帧且256KiB,预算较小时缩字节上限。默认同步;显式开启预取时,6当前批交付前可再预付并授权一批,使子读取与父计算重叠。取消或当前批处理失败时7可能额外读出这一批;预约/发送失败则当前批也不交付。预算不足时同步读取。8最后数据批可含真实EOF,下一次next完成退出/关闭才finished。9低层默认full/source保旧协议;auto结合固定requested范围使用v4 READY与CXB2,10原帧数独立于时间数量,计数和预取不会因跳过无用时间而丢失读取进度。11"""1213from functools import partial14from dataclasses import replace15from pathlib import Path1617from .batch_wire import (BATCH_MAGIC, DEMAND_MAGIC, HEADER, DEMAND_HEADER, MAX_BATCH_BYTES, MAX_BATCH_ROWS, MIN_BATCH_BYTES,18                         FILE_HEADER, FILE_MAGIC, decode_batch)19from .batch_worker import REQUESTED_VALUES_POLICY, batch_worker_main20from .process_read import LegacyCANProcessRead21from .records import MAX_WIRE_BYTES, decode_packet22from ..results.budget import _reserve_owned_ram, _release_owned_ram23from ..results.errors import BudgetExceededError24from ..sources.protocol import SourceReadError252627class LegacyCANBatchRead(LegacyCANProcessRead):28    """持有当前借用结果及可选下一批许可,全部费用由共享关闭流程归还。2930    _current保存已验证批次,_current_cost预付两端全部批次工作空间;31    _prefetch_packet/_prefetch_cost独立持有至多一个下一批许可及其完整费用,32    所有共享账本操作均在父调用线程完成,没有后台线程预约或无界队列。33    _batch_eof仅表示收到EOF候选,实际.finished仍要求子正常退出和父关闭成功。34    """3536    def __init__(self, request, budget, *, frame_ids=None, batch_rows=MAX_BATCH_ROWS,37                 consumer_headroom_bytes=0, prefetch=False, decode_scope='all',38                 decode_frame_ids=None, input_check='full', time_scope='source', _worker_target=None,39                 input_paths=None, decode_policy='legacy', cycle_fields=(), metadata_fields=()):40        """保存批次上限与解码后选择,不打开文件或启动子进程。4142        参数: request/budget同逐条read;frame_ids=None保全部原记录,tuple仅保这些ID;43            batch_rows为1至4096原帧上限;consumer_headroom_bytes为调用方处理本批时44            仍需预约的工作空间,此处只留余量、不代其付款;prefetch为显式bool,45            True允许额外一个已付批次在途;_worker_target仅供真实spawn故障测试。46            decode_scope='all'沿用原全解码与v1 READY,此时decode_frame_ids须为None;47            'requested'须给固定原生tuple ID,fields只能属于这些ID,使用严格v3 READY;48            此时frame_ids仍可为None或其他ID,保留原始帧不扩大固定解码范围。49            input_paths为原序BLF Path tuple或None;decode_policy默认legacy,strict/partial50            采用新长度政策。两者启用时v5 READY确认文件和政策,多文件批带文件位置。51            cycle_fields为另行请求周期的有限字段tuple;metadata_fields仅取候选。两者不加入值解码,v5确认声明。52            input_check默认full/source保旧行为;auto启用ID优先和v4 READY,53            time_scope为source/selected/none,后两者只传所需身份不能供原始片段导出。54        返回: 尚未启动read。异常: 原配置/预算错误,未返回owner仍可恢复close。55        """56        self._frame_ids, self._batch_rows = frame_ids, batch_rows57        self._consumer_headroom_bytes = consumer_headroom_bytes58        self._batch_limit = MAX_WIRE_BYTES59        self._batch_eof = False60        self._frames_authorized = self._records_returned = 061        self._prefetch = prefetch62        self._prefetch_packet = None63        self._prefetch_cost = 064        self._prefetch_headroom_bytes = 065        # 内部来源证明可借用刚核完的原包;默认无观察者,不复制包或另收取散列费用。66        self._validated_packet_consumer = None67        # 基类构造取消会动态调用本类close,先建立策略引用与独立费用责任槽。68        self._decode_scope, self._decode_frame_ids = decode_scope, decode_frame_ids69        self._input_check, self._time_scope = input_check, time_scope70        self._minimum_batch_bytes = MIN_BATCH_BYTES + (71            DEMAND_HEADER.size-HEADER.size if input_check == 'auto' and time_scope == 'source' else 0)72        self._cycle_fields = cycle_fields73        self._metadata_fields = metadata_fields74        self._input_paths, self._decode_policy = input_paths, decode_policy75        self._input_index_accepted = 076        self._source_policy_cost = 077        self._file_envelope = False78        self._full_checks_accepted = 079        self._decode_policy_cost = 080        super().__init__(request, budget,81            _worker_target=batch_worker_main if _worker_target is None else _worker_target)82        try:83            if type(prefetch) is not bool:84                raise TypeError('CAN批次预取开关必须是bool')85            if type(batch_rows) is not int or not 1 <= batch_rows <= MAX_BATCH_ROWS:86                raise ValueError('批次需要1至4096条原帧上限')87            if type(consumer_headroom_bytes) is not int or consumer_headroom_bytes < 0:88                raise ValueError('消费批次的后续工作余量必须是非负整数')89            if frame_ids is not None and (type(frame_ids) is not tuple or len(frame_ids) > 409690                    or any(type(value) is not int or not 0 <= value <= 0x1fffffff for value in frame_ids)91                    or len(set(frame_ids)) != len(frame_ids)):92                raise ValueError('解码后保留ID必须是无重复29位整数tuple或None')93            if type(decode_scope) is not str or decode_scope not in ('all', 'requested'):94                raise ValueError('CAN解码范围需要all或requested')95            if type(input_check) is not str or input_check not in ('auto', 'full'):96                raise ValueError('CAN输入检查需要auto/full')97            if type(time_scope) is not str or time_scope not in ('source', 'selected', 'none'):98                raise ValueError('CAN时间范围需要source/selected/none')99            if input_check == 'full' and time_scope != 'source':100                raise ValueError('完整检查必须保留全来源时间')101            if input_check == 'auto' and decode_scope != 'requested':102                raise ValueError('自动按需检查需要固定requested解码范围')103            if decode_scope == 'all':104                if decode_frame_ids is not None:105                    raise ValueError('全解码模式不能另设固定解码ID')106            else:107                if (type(decode_frame_ids) is not tuple or len(decode_frame_ids) > 4096108                        or any(type(value) is not int or not 0 <= value <= 0x1fffffff109                               for value in decode_frame_ids)):110                    raise ValueError('按需解码ID必须是无重复29位整数tuple')111                # 独立预付父/子tuple、集合、partial及策略元数据;不挪用消费批次余量。112                # 每ID 256B覆盖两侧容器槽,8192B覆盖空容器/函数描述;每字段1024B113                # 另覆盖子侧投影表和构建临时槽,名称借自已付请求,不复制其文本。114                _reserve_owned_ram(self, 8192 + 256 * len(decode_frame_ids)115                                   + 1024 * len(request.fields), '_decode_policy_cost')116                if len(set(decode_frame_ids)) != len(decode_frame_ids):117                    raise ValueError('按需解码ID必须是无重复29位整数tuple')118                if any(field.frame_id not in decode_frame_ids for field in request.fields):119                    raise ValueError('所需字段的报文ID必须属于固定解码集合')120                if _worker_target is None:121                    self._worker_target = partial(batch_worker_main, decode_frame_ids=decode_frame_ids,122                        input_check=input_check, time_scope=time_scope)123            from .decode_policy import validate_decode_policy124            validate_decode_policy(decode_policy)125            from .dbc_cycles import require_cycle_fields126            require_cycle_fields(cycle_fields)127            require_cycle_fields(metadata_fields)128            if input_paths is not None:129                if (type(input_paths) is not tuple or not 1 <= len(input_paths) <= 4096130                        or any(not isinstance(p,Path) or p.suffix.lower() != '.blf'131                               or len(str(p)) > 4096 for p in input_paths)132                        or input_paths[0] != request.input_path):133                    raise ValueError('连续输入需要原序BLF Path tuple,首路径须与INIT一致')134            if input_paths is not None or decode_policy != 'legacy' or cycle_fields or metadata_fields:135                paths = input_paths or (request.input_path,)136                _reserve_owned_ram(self,16384 + sum(512+len(str(p))*32 for p in paths)137                    + len(request.fields)*512138                    + sum(8192+128*len(f.signal) for f in (*cycle_fields,*metadata_fields)),'_source_policy_cost')139                self._file_envelope = len(paths) > 1140                self._minimum_batch_bytes += FILE_HEADER.size if self._file_envelope else 0141                if _worker_target is None:142                    self._worker_target = partial(batch_worker_main,143                        decode_frame_ids=decode_frame_ids,input_check=input_check,time_scope=time_scope,144                        input_paths=None if input_paths is None else tuple(str(p) for p in input_paths),145                        decode_policy=decode_policy,cycle_fields=cycle_fields,metadata_fields=metadata_fields)146        except BaseException as exc:147            self._fail(exc, '批次构造')148149    def _check_ready(self, packet):150        """默认沿用原v1验证;requested核v3,auto核v4及检查/时间范围。151152        参数: packet为严格wire解码后的READY。返回: None。异常: SourceReadError。153        ID先逐项核原生int再比较tuple,拒绝bool/int混同、缺项、额外字段或顺序改变;154        显式测试worker也必须确认同策略,不能用旧v1/v2应答冒充字段已裁剪。155        """156        if self._input_paths is not None or self._decode_policy != 'legacy' or self._cycle_fields or self._metadata_fields:157            expected_paths = [str(p) for p in (self._input_paths or (self._request.input_path,))]158            base_version = 1 if self._decode_scope == 'all' else (4 if self._input_check == 'auto' else 3)159            if (type(packet.get('version')) is not int or packet['version'] != 5160                    or type(packet.get('base_version')) is not int or packet['base_version'] != base_version161                    or packet.get('input_paths') != expected_paths162                    or packet.get('decode_policy') != self._decode_policy):163                raise SourceReadError('连续CAN READY文件或解码政策不符')164            if self._cycle_fields:165                from .records import field_from_dict166                actual = packet.get('cycle_fields')167                if type(actual) is not list or tuple(field_from_dict(f) for f in actual) != self._cycle_fields:168                    raise SourceReadError('CAN READY周期请求与声明不同')169            if self._metadata_fields:170                from .records import field_from_dict171                actual = packet.get('metadata_fields')172                if type(actual) is not list or tuple(field_from_dict(f) for f in actual) != self._metadata_fields:173                    raise SourceReadError('CAN READY字段候选请求与声明不同')174            packet = dict(packet)175            if self._metadata_fields:176                packet.pop('metadata_fields')177            if self._cycle_fields:178                packet.pop('cycle_fields')179            packet['version'] = packet.pop('base_version')180            packet.pop('input_paths'); packet.pop('decode_policy')181        if self._decode_scope == 'all':182            return super()._check_ready(packet)183        required_keys = {'op', 'version', 'profile', 'worker_pid', 'fields',184                           'dependency_versions', 'decode_scope', 'decode_frame_ids',185                           'decoded_values_policy'}186        if self._input_check == 'auto':187            required_keys.update(('input_check', 'time_scope'))188        if (set(packet) != required_keys189                or packet.get('op') != 'ready' or type(packet.get('version')) is not int190                or packet.get('version') != (4 if self._input_check == 'auto' else 3)191                or packet.get('profile') != self._request.profile192                or type(packet.get('worker_pid')) is not int193                or packet.get('worker_pid') != self._worker_pid):194            raise SourceReadError('按需CAN READY身份、版本或实际PID不符')195        ids = packet['decode_frame_ids']196        if (type(packet['decode_scope']) is not str or packet['decode_scope'] != 'requested'197                or type(ids) is not list or len(ids) != len(self._decode_frame_ids)198                or any(type(value) is not int for value in ids)199                or tuple(ids) != self._decode_frame_ids200                or type(packet['decoded_values_policy']) is not str201                or packet['decoded_values_policy'] != REQUESTED_VALUES_POLICY):202            raise SourceReadError('按需CAN READY解码策略或固定ID与启动请求不同')203        if self._input_check == 'auto' and (packet['input_check'] != 'auto'204                or packet['time_scope'] != self._time_scope):205            raise SourceReadError('按需CAN READY检查或时间范围与启动请求不同')206        self._check_ready_metadata(packet)207208    def _check_ready_metadata(self, packet):209        """周期只为显式请求字段核验;默认维持原元数据契约。210211        参数: packet为已核READY。返回: None。异常: 原字段、周期或依赖校验错误。212        """213        return super()._check_ready_metadata(packet,cycle_fields=self._cycle_fields,metadata_fields=self._metadata_fields)214215    @property216    def counters(self):217        """区分批次许可上界、收到的原帧与保留记录,不把未消费批次伪称已完成。218219        参数: 无。返回: 独立dict;frames_authorized包含许可但可能没读到的余量。220        异常: 原小字典分配错误。边界: items计全部原帧;accepted为接收校验完成,221        returned为已交付,records_returned计已交付的保留记录,预取失败不会虚增交付数。222        """223        # 完整保留历史诊断字典形状,新检查事实通过独立属性与版本化批次发布。224        return dict(super().counters, frames_authorized=self._frames_authorized,225                    records_returned=self._records_returned)226227    @property228    def full_check_count(self):229        """读取已验收批次中非原生输入执行完整检查的实际回退数。230231        参数: 无。返回: 非负int;不计尚在预取中的包,关闭后仍保留统计。232        异常: 无。此属性不把完整检查默认模式中的原帧数误作按需回退数量。233        """234        return self._full_checks_accepted235236    def _receive_reply(self):237        """在已预付的上限内接收批数据或原严格控制JSON,拒绝错误版本和超长包。238239        参数: 无。返回: 批次回应dict或原READY/ERROR/STATUS。异常: 原管道/格式/取消错误。240        内部观察者仅在整包严格解码成功后调用一次;只借用本槽中的payload与batch,241        不改变所有权。回调失败沿next的同一read清理,不交付或再次处理该批。242        """243        batch = None244        try:245            self._pending_reply = self._connection.recv_bytes(self._batch_limit)246            if self._pending_reply.startswith((BATCH_MAGIC, DEMAND_MAGIC, FILE_MAGIC)):247                if self._current_cost == 0:248                    raise SourceReadError('未授予/付费的批次数据')249                payload = self._pending_reply250                index, allowance = 0, self._batch_limit251                if self._file_envelope:252                    if len(payload) < FILE_HEADER.size or not payload.startswith(FILE_MAGIC):253                        raise SourceReadError('连续CAN缺少文件位置')254                    _, index = FILE_HEADER.unpack_from(payload)255                    if not self._input_index_accepted <= index < len(self._input_paths):256                        raise SourceReadError('连续CAN文件位置越界或倒退')257                    payload = payload[FILE_HEADER.size:]258                    allowance -= FILE_HEADER.size259                elif payload.startswith(FILE_MAGIC):260                    raise SourceReadError('单文件CAN收到未声明的连续包')261                batch, eof = decode_batch(payload, expected_start=self._items_accepted,262                    row_limit=self._batch_rows, byte_limit=allowance, frame_ids=self._frame_ids,263                    time_scope=self._time_scope, input_check=self._input_check)264                if self._file_envelope:265                    if eof and index != len(self._input_paths)-1:266                        raise SourceReadError('连续CAN在最后文件前报告EOF')267                    batch = replace(batch,input_index=index)268                    self._input_index_accepted = index269                if self._validated_packet_consumer is not None:270                    self._validated_packet_consumer(self, self._pending_reply, batch)271                return {'op': 'batch', 'request_id': batch.start_sequence, 'batch': batch, 'eof': eof}272            # 控制包即使在批读取时也保持原64KiB与严格JSON边界。273            return decode_packet(self._pending_reply)274        finally:275            # 原错误可留在调用方;已结束调用的临时引用不继续占有整批对象。276            batch = payload = None277278    def __next__(self):279        """授予一批有界许可并交付原序数据;确认真实EOF及退出后才结束迭代。280281        参数: 无。返回: LegacyDecodedBatch,借至下次next或close。282        异常: 真StopIteration、原预算/来源/取消错误;持续清理失败保同一owner。283        """284        if self._finished:285            raise StopIteration286        self._require_open()287        reply = batch = packet = None288        try:289            self.start()290            # 同步入口保持原归还时机;预取入口的最后批费用在实际关闭后归还。291            if not (self._prefetch and self._batch_eof):292                self._current = self._pending_reply = None293                _release_owned_ram(self, self._current_cost, '_current_cost')294            if self._batch_eof:295                self._eof_received = True296                self.close()297                if self._exit_code != 0:298                    raise SourceReadError('旧CAN批次发EOF但没有正常退出')299                self._finished = True300            else:301                if self._prefetch_packet is not None:302                    packet = self._prefetch_packet303                    if packet['request_id'] != self._items_accepted:304                        raise SourceReadError('预取许可与实际已消费位置不符')305                    self._batch_limit = packet['bytes']306                    # 同一owner的两槽之间移交已付责任,没有新的预算回调或线程。307                    self._current_cost, self._prefetch_cost = self._prefetch_cost, 0308                    self._prefetch_packet = None309                    reply = self._receive_response(packet)310                else:311                    self._batch_limit = self._available_limit()312                    if self._batch_limit < self._minimum_batch_bytes:313                        raise BudgetExceededError('CAN批次预算不足以保留一条最大记录及两端工作空间')314                    _reserve_owned_ram(self, 128*self._batch_limit, '_current_cost')315                    packet = self._new_packet(self._batch_limit)316                    self._pulls_issued += 1317                    self._frames_authorized += self._batch_rows318                    reply = self._exchange(packet)319                if reply.get('op') != 'batch' or reply.get('request_id') != packet['request_id']:320                    raise SourceReadError('批次许可未得到对应批次回应')321                batch = reply['batch']322                self._batch_eof = reply['eof']323                self._items_accepted += batch.frame_count324                self._full_checks_accepted += batch.full_check_count325                self._current = batch326                self._pending_reply = None327                if self._prefetch:328                    self._try_prefetch()329                # 预取预约/发送失败时当前批未交付;accepted保接收事实,returned不提前增加。330                self._items_returned += batch.frame_count331                self._records_returned += len(batch.records)332                return batch333        except BaseException as exc:334            # 清掉本frame对大批对象的额外引用;真正持有关系由父_current负责清理。335            reply = batch = packet = None336            self._fail(exc, '批次读取')337        finally:338            reply = batch = packet = None339        raise StopIteration340341    def _new_packet(self, byte_limit):342        """为下一原序号建立有限批次许可,不触碰管道。343344        参数: 已确定并付费的字节上限。返回: 小dict。异常: 原分配错误。345        """346        return {'op': 'pull_batch', 'request_id': self._items_accepted,347                'rows': self._batch_rows, 'bytes': byte_limit,348                'frame_ids': None if self._frame_ids is None else list(self._frame_ids)}349350    def _set_prefetch_headroom(self, extra_bytes):351        """更新仅供下一次额外预取的动态消费余量,不收费且不改变同步许可。352353        参数: extra_bytes为调用方证明所需的非负原生int,在每次next前更新。354        返回: None。异常: TypeError/ValueError为非法余量;关闭后拒绝更新。355        边界: 本线程直接设置有限整数,没有后台预算回调;不足时只停预取。356        """357        self._require_open()358        if type(extra_bytes) is not int:359            raise TypeError('预取额外消费余量必须是原生int')360        if extra_bytes < 0:361            raise ValueError('预取额外消费余量不能为负数')362        self._prefetch_headroom_bytes = extra_bytes363364    def _available_limit(self, *, additional_headroom=0):365        """在当前共享余额中留下消费者余量,计算完整可付款的批次上限。366367        参数: additional_headroom为预取专用额外余量,原同步读取固定为0。368        返回: 不超过MAX_BATCH_BYTES的整数,可能不足一条。369        异常: 原账本读取错误;这里只看余额,不取得额外空间。370        """371        available = (self._budget.memory_budget_bytes - self._budget.ram_bytes372                     - self._consumer_headroom_bytes - additional_headroom)373        return min(MAX_BATCH_BYTES, available // 128)374375    def _try_prefetch(self):376        """余量足够时全额预付并发送下一批;否则留到下一次同步读取。377378        参数: 无,当前批已完成全部接包校验。返回: None。379        异常: 原预约/发送/取消错误;失败不重发,也不交付当前批,由next统一清理。380        边界: EOF不再发许可;至多一个在途批,消费余量不用于预付。381        """382        if self._batch_eof:383            return384        if self._waiting or self._prefetch_packet is not None or self._prefetch_cost:385            raise SourceReadError('预取已有一个在途请求或费用责任')386        limit = self._available_limit(additional_headroom=self._prefetch_headroom_bytes)387        if limit < self._minimum_batch_bytes:388            return389        # 只有付款前的余额不足可以回退;预约实际入账或发送中断不可猜测成功程度。390        _reserve_owned_ram(self, 128*limit, '_prefetch_cost')391        self._prefetch_packet = self._new_packet(limit)392        self._pulls_issued += 1393        self._frames_authorized += self._batch_rows394        self._send_request(self._prefetch_packet)395396    def _release_closed_state(self):397        """共享close确认实际资源全关后清预取责任,再按原槽逐笔归还。398399        参数: 无。返回: None。异常: 原退费错误;同一close可从实际剩余槽恢复。400        """401        self._prefetch_packet = self._frame_ids = None402        self._validated_packet_consumer = None403        # 实际进程已结束才清策略与partial;退款失败仍保本owner费用槽供重复close。404        self._decode_frame_ids = None405        self._worker_target = None406        self._input_paths = None407        self._cycle_fields = ()408        self._metadata_fields = ()409        _release_owned_ram(self,self._source_policy_cost,'_source_policy_cost')410        _release_owned_ram(self, self._decode_policy_cost, '_decode_policy_cost')411        _release_owned_ram(self, self._prefetch_cost, '_prefetch_cost')412        super()._release_closed_state()413
Chronaxis 用户手册 · 离线阅读