Chronaxis用户手册English
本页目录

chronaxis.results.raw_exports

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""保存原始片段索引并复制已生成的BLF,不重新调用旧读写或解码路径。23输入是实际Segment范围、已关闭的BLF文件及其明确摘要,输出是连续编号的有界4JSONL索引和发布目录内的逐文件副本。exported保留正长actual,即使文件含零帧;5empty_range保留已知零长度actual;no_observation仅用于actual为None,不造空文件。6append核实际BLF字节/SHA后写索引;seal真正关闭writer才开放独立reader。reader只有7完整EOF、流SHA/数量与真实关闭均成功后才verified_eof,提前关闭不能证明完整。89所有新增RAM与索引spool先预约,原BLF由旧writer封装预约,本模块不重复计算。10copy_to只逐条读索引、有限块复制和核现成BLF;发布副本不计入分析暂存配额。11输出失败只清本次复制资源,保sealed输入与其他reader,恢复后可向新目录重试;12原索引或原BLF内容损坏仍使输入仓failed,不把坏输入当作可发布结果。13所有真实FD、未返回reader和费用槽由本store可达,关闭失败可重试,保原异常/取消14及最高raw_export_store_cleanup_owner;文件与累计spool保留,不凭共享余额补账。15本模块只依赖标准库、冻结模型和有界JSON,不导入CAN库,不声称整进程RSS受限。16"""1718# 固定模型与有界编码不触及原日志/DBC;资源交易复用新结果层已经验证的协议。19from dataclasses import dataclass20from hashlib import sha25621from pathlib import Path22from re import fullmatch23from ..core.models import Interval24from ..core.validation import require_identifier25from ..legacy.records import MAX_WIRE_BYTES, encode_packet, decode_packet26from ..timebase.conversion import INT64_MAX, require_int64_ns27from .budget import ResourceBudget28from .errors import ResultClosedError, ResultFormatError29from .spool import _cleanup_owner, _reserve_owned_ram, _release_owned_ram303132_FIELDS = ('index', 'source', 'session_id', 'segment_id', 'window_id', 'requested',33           'actual', 'frame_count', 'file_name', 'sha256', 'byte_count', 'status', 'device_label')34_ROW_COST = 65536 + 64 * MAX_WIRE_BYTES35_CHUNK_BYTES = 6553636_IO_COST = 8 * _CHUNK_BYTES37_MAX_INDEX = 999999999999383940def _integer(value, name, maximum=INT64_MAX):41    """校验非负原生整数,不把bool或浮点作为数量。4243    参数: value候选;name错误位置;maximum含端点上限。返回: 原int。44    异常: TypeError/ValueError为类型或范围错误;不截断、不转换。45    """46    if type(value) is not int:47        raise TypeError(name + '需要原生int')48    if not 0 <= value <= maximum:49        raise ValueError(name + '超出允许范围')50    return value515253def _text(value, name, *, identifier=False):54    """检查有界安全原文本,保留大小写、Unicode和合法ID标点。5556    参数: value原文本;name位置;identifier要求来源/会话简单名。返回: 原str。57    异常: TypeError/ValueError为非原生str、空白、控制字符、过长或名称非法。58    """59    if type(value) is not str:60        raise TypeError(name + '需要原生str')61    if not value.strip() or len(value) > MAX_WIRE_BYTES or any(ord(char) < 32 or ord(char) == 127 for char in value):62        raise ValueError(name + '为空、含控制字符或超过有界文本能力')63    if identifier:64        require_identifier(value, name)65    return value666768def _interval(value, name):69    """要求冻结Interval及原生int64端点,不重建或改变原范围。7071    参数: value范围;name位置。返回: 原Interval。72    异常: TypeError/ValueError/OverflowError为类型、顺序或整数范围非法。73    """74    if type(value) is not Interval:75        raise TypeError(name + '需要Interval')76    for endpoint in (value.start_ns, value.end_ns):77        if type(endpoint) is not int:78            raise TypeError(name + '端点需要原生int64')79        require_int64_ns(endpoint, name)80    require_int64_ns(value.end_ns - value.start_ns, name + '.duration')81    if value.end_ns < value.start_ns:82        raise ValueError(name + '不能反向')83    return value848586def _packet(record):87    """把固定模型转为固定JSON树,不复制文件内容或任意元数据。8889    参数: record为RawExportRecord。返回: 仅13字段dict。90    异常: 内部模型属性错误传播;调用者另核整包64KiB上限。91    """92    result = {name: getattr(record, name) for name in _FIELDS}93    for name in ('requested', 'actual'):94        interval = result[name]95        result[name] = None if interval is None else dict(start_ns=interval.start_ns, end_ns=interval.end_ns)96    return result979899@dataclass(frozen=True)100class RawExportRecord:101    """保存一个原片段导出事实;构造校声明,不据此宣称文件已读取或写出。"""102103    index: int  # 从0连续;对应唯一12位文件名,非业务排序键。104    source: str  # 同一store的CAN来源安全名。105    session_id: str  # 原会话安全名。106    segment_id: str  # 实际Segment的原ID。107    window_id: str  # 产生原片段的窗口ID。108    requested: Interval  # 保留完整原请求范围。109    actual: Interval | None  # 原实际交集;已知点与无观测严格区分。110    frame_count: int  # 原writer成功交付的帧数;正长范围可为0。111    file_name: str | None  # exported固定为raw_segments/12位.blf。112    sha256: str | None  # 实际输出BLF字节SHA,不是原输入SHA。113    byte_count: int  # 实际输出文件字节数。114    status: str  # exported/empty_range/no_observation。115    device_label: str  # 本条产生设备标签,与store严格一致。116117    def __post_init__(self):118        """检查数量、身份、范围和文件状态的闭合关系,整条记录有界。119120        参数: 无,读取冻结字段。返回: None。121        异常: TypeError/ValueError/OverflowError为非法事实;不打开、stat或创建文件。122        """123        _integer(self.index, 'raw_export.index', _MAX_INDEX)124        _integer(self.frame_count, 'raw_export.frame_count')125        _integer(self.byte_count, 'raw_export.byte_count')126        for name in ('source', 'session_id', 'segment_id', 'window_id', 'device_label'):127            _text(getattr(self, name), name, identifier=name in ('source', 'session_id'))128        _interval(self.requested, 'raw_export.requested')129        if self.actual is not None:130            _interval(self.actual, 'raw_export.actual')131            if self.actual.start_ns < self.requested.start_ns or self.actual.end_ns > self.requested.end_ns:132                raise ValueError('raw_export.actual必须落在原requested内')133        if type(self.status) is not str or self.status not in ('exported', 'empty_range', 'no_observation'):134            raise ValueError('原始导出状态非法')135        if self.status == 'exported':136            if self.actual is None or self.actual.start_ns == self.actual.end_ns:137                raise ValueError('exported需正长实际范围')138            if type(self.file_name) is not str or self.file_name != f'raw_segments/{self.index:012d}.blf':139                raise ValueError('原始导出文件名必须对应当前连续索引')140            if type(self.sha256) is not str or fullmatch(r'[0-9a-fA-F]{64}', self.sha256) is None:141                raise ValueError('原始导出SHA需64位十六进制')142        else:143            expected = self.actual is None if self.status == 'no_observation' else self.actual is not None and self.actual.start_ns == self.actual.end_ns144            if not expected or self.file_name is not None or self.sha256 is not None or self.frame_count or self.byte_count:145                raise ValueError('无观测/零范围必须保原actual且不得带文件、SHA或帧字节数')146        # 在模型边界按现有wire能力校整包,避免多个单独合法长文本拼成无界行。147        if len(encode_packet(_packet(self))) + 1 > MAX_WIRE_BYTES:148            raise ValueError('原始导出索引行超过64KiB')149150151def _decode(payload):152    """严格还原一条固定索引,不接受多键、重复JSON键或浮点端点。153154    参数: payload为不含换行的有限bytes。返回: RawExportRecord。155    异常: ResultFormatError保留底层结构/类型/时间校验原因;取消原样传播。156    """157    try:158        value = decode_packet(payload)159        if set(value) != set(_FIELDS):160            raise ValueError('原始导出索引字段缺失或多余')161        for name in ('requested', 'actual'):162            interval = value[name]163            if interval is None and name == 'actual':164                continue165            if type(interval) is not dict or set(interval) != {'start_ns', 'end_ns'}:166                raise ValueError('索引范围必须精确含start_ns/end_ns')167            if type(interval['start_ns']) is not int or type(interval['end_ns']) is not int:168                raise TypeError('索引范围端点必须为原生int')169            value[name] = Interval(interval['start_ns'], interval['end_ns'])170        return RawExportRecord(**value)171    except (TypeError, ValueError, OverflowError, UnicodeError) as exc:172        raise ResultFormatError('原始导出索引行非法: ' + str(exc)) from exc173174175def _write_all(handle, payload):176    """完整写有限bytes,短写继续,零写或非法返回立即失败。177178    参数: handle真实无缓冲FD;payload有限bytes。返回: None。179    异常: 原write/取消或OSError;费用由调用方先预约,不在此重复计算。180    """181    view = memoryview(payload)182    while view:183        written = handle.write(view)184        if type(written) is not int or not 0 < written <= len(view):185            raise OSError('原始导出文件写入未正确推进')186        view = view[written:]187188189class RawExportStore:190    """拥有索引、活reader与当前复制FD;状态与真实closed事实分别记录。191192    building只可append/seal;sealed可独立回读或重复copy。构建或输入完整性失败193    转failed;普通复制输出失败仅恢复本次资源。关闭中只允许重试close,不丢槽。194    """195196    format = 'blf'197198    def __init__(self, budget, work_dir, source, session_id, device_label='pending'):199        """先付固定费用,再在全新根独占创建目录和索引writer。200201        参数: budget为借用ResourceBudget;work_dir为全新str/Path;source/session_id202            为安全名;device_label为有界原标签,默认pending。203        返回: building仓。异常: 原声明/路径/预算/IO/取消;持续清理失败附最高恢复owner。204        """205        if not isinstance(budget, ResourceBudget) or not isinstance(work_dir, (str, Path)):206            raise TypeError('原始导出仓需ResourceBudget及显式新目录')207        _text(source, 'raw_exports.source', identifier=True)208        _text(session_id, 'raw_exports.session_id', identifier=True)209        _text(device_label, 'raw_exports.device_label')210        if sum(len(part) for part in Path(work_dir).parts) > MAX_WIRE_BYTES:211            raise ValueError('原始导出工作路径超过有界能力')212        self.budget = self._budget = budget213        self.source, self.session_id, self.device_label = source, session_id, device_label214        self.path = self.inventory_path = self._raw_path = None215        self._writer = self._pending_reader = self._copy_reader = None216        self._input_fd = self._output_fd = self._inventory_fd = None217        self._readers = self._digest = self._payload = self._chunk = None218        self._fixed_cost = self._append_cost = self._io_cost = self._copy_cost = self._held_cost = 0219        self._spool_bytes = self.inventory_bytes = self.record_count = self.file_count = 0220        self.sha256 = None221        self.state, self._closed, self._closing = 'building', False, False222        try:223            # 路径/来源标签及活reader登记均有固定费用,首笔后取消也可从完整槽恢复。224            requested = Path(work_dir)225            _reserve_owned_ram(self, 65536 + 32 * (len(str(requested)) + len(source) + len(session_id) + len(device_label)), '_fixed_cost')226            resolved = requested.resolve()227            resolved.mkdir(parents=True, exist_ok=False)228            self.path, self._raw_path = resolved, resolved / 'raw_segments'229            self._raw_path.mkdir(exist_ok=False)230            self.inventory_path = resolved / 'raw_segments.jsonl'231            self._readers, self._digest = set(), sha256()232            self._writer = self.inventory_path.open('xb', buffering=0)233        except BaseException as exc:234            self._fail(exc)235            raise236237    @property238    def closed(self):239        """读取真实关闭事实。参数: 无。返回: bool。异常: 无,FD或费用未清不为True。"""240        return self._closed241242    @property243    def spool_bytes(self):244        """读取仅本索引新增的累计配额。参数: 无。返回: int。异常: 无,不含BLF或发布副本。"""245        return self._spool_bytes246247    def _require(self, state):248        """检查活状态,半关闭或失败前缀不能继续使用。249250        参数: state为building/sealed。返回: None。异常: ResultClosedError/RuntimeError。251        """252        if self._closed or self._closing:253            raise ResultClosedError('原始导出仓已关闭或正在清理')254        if self.state != state:255            raise RuntimeError('原始导出仓状态不允许操作: ' + self.state)256257    def _fail(self, exc):258        """保原主错/取消、failed状态和最高恢复owner,清理副错只记note。259260        参数: exc为实际主异常。返回: None。异常: 清理错误不替代原exc。261        """262        self.state = 'failed'263        exc.raw_export_store_cleanup_owner = self264        _cleanup_owner(self, exc, 'raw_export_store_cleanup_owner', '原始导出仓')265266    def next_path(self):267        """计算当前记录对应BLF路径,不创建文件、不跳过无观测索引。268269        参数: 无。返回: 根/raw_segments/{record_count:012d}.blf。270        异常: 状态错误或索引超过12位能力;原writer由调用方独立持有和关闭。271        """272        self._require('building')273        _integer(self.record_count, 'raw_exports.record_count', _MAX_INDEX)274        return self._raw_path / f'{self.record_count:012d}.blf'275276    def _relation(self, record, index):277        """核本仓归属和连续索引,不从文件名猜来源。278279        参数: record为冻结记录;index为当前预期号。返回: None。异常: TypeError/ValueError。280        """281        if type(record) is not RawExportRecord:282            raise TypeError('原始导出仓仅接受RawExportRecord')283        if (record.index != index or record.source != self.source or record.session_id != self.session_id284                or record.device_label != self.device_label):285            raise ValueError('原始导出记录索引/来源/会话/设备不属于本仓')286287    def _close_fd(self, slot):288        """真实FD关闭后才撤槽,关闭后抛错也依据actual.closed处理。289290        参数: slot为本owner固定FD槽。返回: None。异常: 原close/取消,持续失败可重试。291        """292        handle = getattr(self, slot)293        if handle is not None:294            try:295                handle.close()296            finally:297                if handle.closed:298                    setattr(self, slot, None)299300    def _file(self, record, destination=None):301        """有限块核实际BLF;可同时复制到独占目标,不调用任何CAN读写器。302303        参数: record为exported记录;destination为None或新文件Path。返回: None。304        异常: 原预算/IO/取消或ResultFormatError;所有当前FD仍从本store可达。305        """306        if self._input_fd is not None or self._output_fd is not None or self._io_cost:307            raise ResultClosedError('原始导出仓有未完成文件资源待清理')308        _reserve_owned_ram(self, _IO_COST, '_io_cost')309        try:310            self._input_fd = (self.path / record.file_name).open('rb', buffering=0)311        except FileNotFoundError as exc:312            # 索引已指明存在的原件消失是输入失效;不能当成本次发布目录错误重试。313            self._fail(exc)314            raise315        if destination is not None:316            self._output_fd = destination.open('xb', buffering=0)317        digest, count = sha256(), 0318        # 原件和副本同时只持一个块;超过声明字节立即拒绝,不扫无界额外后缀。319        while True:320            self._chunk = self._input_fd.read(_CHUNK_BYTES)321            if not self._chunk:322                break323            count += len(self._chunk)324            if count > record.byte_count:325                exc = ResultFormatError('原始BLF字节数超过索引')326                self._fail(exc)327                raise exc328            digest.update(self._chunk)329            if self._output_fd is not None:330                _write_all(self._output_fd, self._chunk)331        if count != record.byte_count or digest.hexdigest() != record.sha256.lower():332            exc = ResultFormatError('原始BLF字节数或SHA与索引不符')333            self._fail(exc)334            raise exc335        if self._output_fd is not None:336            self._output_fd.flush()337        self._close_fd('_output_fd')338        self._close_fd('_input_fd')339        # 发布字节也以真实文件回读核验,不能只凭write返回长度宣称副本正确。340        if destination is not None:341            self._check_copy(destination, record.byte_count, record.sha256)342        self._chunk = None343        _release_owned_ram(self, self._io_cost, '_io_cost')344345    def _check_copy(self, path, expected_bytes, expected_sha):346        """在已预约IO空间内回读真实发布文件,BLF与索引使用同一有界验证。347348        参数: path为本次新目标Path;expected_bytes/SHA为已封口原事实。返回: None。349        异常: 原打开/read/close/取消或ResultFormatError;当前真实FD留在本owner槽。350        """351        if self._input_fd is not None or not self._io_cost:352            raise ResultClosedError('发布文件回读需空输入槽和已预约IO空间')353        self._input_fd = path.open('rb', buffering=0)354        digest, count = sha256(), 0355        while True:356            self._chunk = self._input_fd.read(_CHUNK_BYTES)357            if not self._chunk:358                break359            count += len(self._chunk)360            if count > expected_bytes:361                raise ResultFormatError('发布文件字节数超过封口事实')362            digest.update(self._chunk)363        if count != expected_bytes or digest.hexdigest() != expected_sha.lower():364            raise ResultFormatError('发布文件字节数或SHA与封口事实不符')365        self._close_fd('_input_fd')366        self._chunk = None367368    def append(self, record):369        """预付索引RAM/累计spool,核实际文件后完整写一行再推进计数。370371        参数: record为本仓当前索引的RawExportRecord。返回: None。372        异常: 原归属/预算/文件/编码/取消;开始后的失败poison,原BLF费用不重复预约。373        """374        self._require('building')375        try:376            self._relation(record, self.record_count)377            _reserve_owned_ram(self, _ROW_COST, '_append_cost')378            self._payload = encode_packet(_packet(record)) + b'\n'379            before = self._budget.spool_bytes380            try:381                self._budget.reserve_spool(len(self._payload))382            finally:383                if self._budget.spool_bytes == before + len(self._payload):384                    self._spool_bytes += len(self._payload)385            if record.status == 'exported':386                self._file(record)387            _write_all(self._writer, self._payload)388            self._digest.update(self._payload)389            self.inventory_bytes += len(self._payload)390            self.record_count += 1391            self.file_count += int(record.status == 'exported')392            self._payload = None393            _release_owned_ram(self, self._append_cost, '_append_cost')394        except BaseException as exc:395            self._fail(exc)396            raise397398    def seal(self):399        """真实关闭索引writer并固定流SHA;零记录亦可封口。400401        参数: 无。返回: None。异常: 原flush/close/取消,失败不能冒充sealed。402        """403        self._require('building')404        try:405            self._writer.flush()406            self._close_fd('_writer')407            self.sha256, self.state = self._digest.hexdigest(), 'sealed'408        except BaseException as exc:409            self._fail(exc)410            raise411412    def iter_records(self):413        """创建有限独立索引reader,未返回构造始终在本父pending槽可达。414415        参数: 无。返回: 逐条RawExportRecord迭代器。异常: 原预算/打开/登记/取消及状态错误。416        """417        self._require('sealed')418        if self._pending_reader is not None:419            raise ResultClosedError('原始导出仓有未返回reader待清理')420        try:421            reader = _RawExportReader(self)422            self._pending_reader = None423            return reader424        except BaseException as exc:425            self._fail(exc)426            raise427428    def copy_to(self, destination):429        """向新发布根独占创建原始目录与索引,逐条核验复制现成BLF。430431        参数: destination为Path;根可已有普通结果表,raw_segments及索引必须均不存在。432        返回: 目标raw_segments.jsonl Path。异常: 原路径/预算/IO/取消或完整性错误;433            输出失败保本次FD责任及部分文件,不覆盖已有工件或关闭sealed输入;434            入口先恢复上次复制,持续关闭失败时不得覆盖旧槽或创建下个目标。435        """436        self._require('sealed')437        self.recover_copy()438        if not isinstance(destination, Path):439            raise TypeError('原始导出发布根需要Path')440        if sum(len(part) for part in destination.parts) > MAX_WIRE_BYTES:441            raise ValueError('原始导出发布路径超过有界能力')442        target = destination.resolve()443        raw_path, inventory = target / 'raw_segments', target / 'raw_segments.jsonl'444        if raw_path.exists() or inventory.exists():445            raise FileExistsError('原始导出发布目录或索引已存在')446        try:447            _reserve_owned_ram(self, _ROW_COST + 32 * len(str(target)), '_copy_cost')448            target.mkdir(parents=True, exist_ok=True)449            raw_path.mkdir(exist_ok=False)450            self._inventory_fd = inventory.open('xb', buffering=0)451            # 本次临时reader先装独立copy槽;其预约/关闭取消不破坏其他已封口输入。452            self._copy_reader = _RawExportReader(self, copy=True)453            digest, count, files = sha256(), 0, 0454            # reader只交付当前一行;不累计文件路径或导出记录列表。455            for record in self._copy_reader:456                if record.status == 'exported':457                    self._file(record, target / record.file_name)458                    files += 1459                self._payload = encode_packet(_packet(record)) + b'\n'460                _write_all(self._inventory_fd, self._payload)461                digest.update(self._payload)462                self._payload = None463                count += 1464            if (not self._copy_reader.verified_eof or count != self.record_count465                    or files != self.file_count or digest.hexdigest() != self.sha256):466                exc = ResultFormatError('原始导出复制索引未完整匹配封口事实')467                self._fail(exc)468                raise exc469            self._copy_reader = None470            self._inventory_fd.flush()471            self._close_fd('_inventory_fd')472            _reserve_owned_ram(self, _IO_COST, '_io_cost')473            self._check_copy(inventory, self.inventory_bytes, self.sha256)474            _release_owned_ram(self, self._io_cost, '_io_cost')475            _release_owned_ram(self, self._copy_cost, '_copy_cost')476            return inventory477        except BaseException as exc:478            # 输入reader或原BLF摘要错误可已poison父;输出故障只收回本次复制责任。479            exc.raw_export_store_cleanup_owner = self480            for attempt in range(2):481                try:482                    self._close_copy_resources()483                    break484                except BaseException as cleanup:485                    exc.add_note(f'原始导出复制清理第{attempt + 1}次失败: {type(cleanup).__name__}')486            raise487488    def recover_copy(self):489        """仅恢复上次输出复制的真实责任,保sealed固定输入及其他独立reader。490491        参数: 无。返回: None;copy_to入口会自动调用,调用方也可显式恢复。492        异常: 原FD/reader/费用关闭错误或状态错误;持续失败保最高store与原槽,493            不把整个输入仓置关闭中,不覆盖上次复制尚未释放的资源。494        """495        self._require('sealed')496        self._close_copy_resources()497498    def _close_copy_resources(self):499        """关闭本次copy子reader和FD,完全释放后再清当前缓冲与复制费用。500501        参数: 无。返回: None;不操作固定费、原索引writer或其他独立reader。502        异常: 首个真实关闭/抵销错误,副错记note;最高store保存未完成实际槽。503        """504        failure = None505        if self._copy_reader is not None:506            try:507                self._copy_reader.close()508            except BaseException as exc:509                failure = exc510            finally:511                if self._copy_reader.closed:512                    self._copy_reader = None513        for slot in ('_input_fd', '_output_fd', '_inventory_fd'):514            try:515                self._close_fd(slot)516            except BaseException as exc:517                if failure is None:518                    failure = exc519                else:520                    failure.add_note('原始导出本次复制FD关闭附加失败: ' + type(exc).__name__)521        if self._copy_reader is None and all(getattr(self, slot) is None for slot in ('_input_fd', '_output_fd', '_inventory_fd')):522            self._payload = self._chunk = None523            for slot in ('_io_cost', '_copy_cost'):524                try:525                    _release_owned_ram(self, getattr(self, slot), slot)526                except BaseException as exc:527                    if failure is None:528                        failure = exc529                    else:530                        failure.add_note('原始导出本次复制费用抵销附加失败: ' + type(exc).__name__)531        if failure is not None:532            failure.raw_export_store_cleanup_owner = self533            raise failure534535    def close(self):536        """关闭全部自有FD/reader并抵销自有RAM,文件与累计spool保留。537538        参数: 无。返回: None,完成后幂等。异常: 首个真实关闭/抵销错误,副错记note;539            持续故障保原槽/最高owner,不能从借用账本总额倒推或多扣费用。540        """541        if self._closed:542            return543        self._closing = True544        if self.state == 'building':545            self.state = 'failed'546        failure = None547        # pending与活登记可短暂指向同一真实子;先pending,成功后从登记中解除。548        readers = []549        for reader in (self._pending_reader, self._copy_reader, *tuple(self._readers or ())):550            if reader is not None and reader not in readers:551                readers.append(reader)552        for reader in readers:553            try:554                reader.close()555            except BaseException as exc:556                if failure is None:557                    failure = exc558                else:559                    failure.add_note('原始导出reader关闭附加失败: ' + type(exc).__name__)560        if self._copy_reader is not None and self._copy_reader.closed:561            self._copy_reader = None562        for slot in ('_writer', '_input_fd', '_output_fd', '_inventory_fd'):563            try:564                self._close_fd(slot)565            except BaseException as exc:566                if failure is None:567                    failure = exc568                else:569                    failure.add_note('原始导出FD关闭附加失败: ' + type(exc).__name__)570        if (self._pending_reader is None and not self._readers and self._copy_reader is None571                and all(getattr(self, slot) is None for slot in ('_writer', '_input_fd', '_output_fd', '_inventory_fd'))):572            self._payload = self._chunk = None573            for slot in ('_append_cost', '_io_cost', '_copy_cost', '_fixed_cost'):574                try:575                    _release_owned_ram(self, getattr(self, slot), slot)576                except BaseException as exc:577                    if failure is None:578                        failure = exc579                    else:580                        failure.add_note('原始导出费用抵销附加失败: ' + type(exc).__name__)581            if self._held_cost == 0:582                self._closed = True583        if failure is not None:584            failure.raw_export_store_cleanup_owner = self585            raise failure586587588class _RawExportReader:589    """拥有单个索引位置/摘要和固定读槽;完整EOF与成功关闭才verified_eof。590591    copy模式只把临时资源错误交本次复制恢复,输入格式/摘要破坏仍poison父仓。592    """593594    def __init__(self, owner, *, copy=False):595        """先装父pending槽,再付费打开/登记真实reader。596597        参数: owner为活sealed仓;copy为内部临时复制模式,直接先装父_copy_reader槽。598        返回: 独立reader。异常: 原预算/打开/登记/取消;真实父槽始终保未返回self。599        """600        self.owner, self._budget = owner, owner.budget601        self._copy = copy602        self.handle = self._digest = None603        self._fixed_cost = self._held_cost = self._count = self._files = self._bytes = 0604        self.closed = self._closing = self.verified_eof = False605        if copy:606            owner._copy_reader = self607        else:608            owner._pending_reader = self609        # 仅索引reader允许8192字节有限预读;buffer/对象另付16384,原行限及真EOF不变。610        # BLF分块复制与写端不改,同一handle始终拥有buffer及真实raw FD。611        _reserve_owned_ram(self, _ROW_COST + 16384, '_fixed_cost')612        self._digest = sha256()613        try:614            self.handle = owner.inventory_path.open('rb', buffering=8192)615        except FileNotFoundError as exc:616            # 普通工厂原已有_fail;copy专用工厂也须把源索引消失归为输入完整性失效。617            if copy:618                owner._fail(exc)619            raise620        owner._readers.add(self)621622    def __iter__(self):623        """保持独立原位置。参数: 无。返回: self。异常: 无,不重新开文件。"""624        return self625626    def __next__(self):627        """逐行校索引归属/序号,真实EOF核完整SHA、条数、文件数与字节数后关闭。628629        参数: 无。返回: RawExportRecord。异常: 真实StopIteration或原格式/关闭/取消;630            输入损坏poison父;copy临时资源失败交本次恢复,原异常/取消对象保持。631        """632        if self.closed:633            raise StopIteration634        self.owner._require('sealed')635        if self._closing:636            raise ResultClosedError('原始导出reader正在清理')637        try:638            payload = self.handle.readline(MAX_WIRE_BYTES + 1)639            if payload:640                if len(payload) > MAX_WIRE_BYTES or not payload.endswith(b'\n'):641                    raise ResultFormatError('原始导出索引行超限或缺完整换行')642                if self._count >= self.owner.record_count:643                    raise ResultFormatError('原始导出索引出现额外后缀')644                record = _decode(payload[:-1])645                try:646                    self.owner._relation(record, self._count)647                except (TypeError, ValueError) as exc:648                    raise ResultFormatError('原始导出索引归属或序号非法') from exc649                self._digest.update(payload)650                self._count += 1651                self._files += int(record.status == 'exported')652                self._bytes += len(payload)653                return record654            if (self._count != self.owner.record_count or self._files != self.owner.file_count655                    or self._bytes != self.owner.inventory_bytes or self._digest.hexdigest() != self.owner.sha256):656                raise ResultFormatError('原始导出索引EOF数量或SHA不匹配')657            self.close()658        except BaseException as exc:659            if not self._copy or isinstance(exc, ResultFormatError):660                self.owner._fail(exc)661            else:662                exc.raw_export_store_cleanup_owner = self.owner663            raise664        self.verified_eof = True665        raise StopIteration666667    def close(self):668        """真实关FD、抵销本笔费用并解除父登记后才closed,可持续重试。669670        参数: 无。返回: None。异常: 原close/预算/取消;最高store仍持本实例。671        """672        if self.closed:673            return674        self._closing = True675        try:676            if self.handle is not None and not self.handle.closed:677                self.handle.close()678            _release_owned_ram(self, self._fixed_cost, '_fixed_cost')679            self.owner._readers.discard(self)680            if self.owner._pending_reader is self:681                self.owner._pending_reader = None682            self.closed = True683        except BaseException as exc:684            exc.raw_export_store_cleanup_owner = self.owner685            raise686
Chronaxis 用户手册 · 离线阅读