Chronaxis用户手册English
本页目录

chronaxis.results.frame_sort

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""为离线报文导出进行有界磁盘排序,不把整段报文留在内存。23输入为有限标量排序键和新导出自己的JSON记录,输出按键稳定排列的记录流。4每批最多64行,归并只开两路输入;旧run留作诊断且累计计入spool。旧调用默认用摘要绑定每行与结束标记;新版仅显式完整性诊断时启用。5不启用摘要时仍校验行结构、行长和结束计数,拒绝截断,不提供防篡改证明。6所有句柄在owner上登记,持续关闭失败仍可重试;本模块不读原日志或加载DBC。7"""89from hashlib import blake2b10from hmac import compare_digest11import json12from pathlib import Path13from secrets import token_bytes1415from .budget import _reserve_owned_ram, _release_owned_ram, _cleanup_owner16from .errors import ResultClosedError, ResultFormatError171819class FrameSort:20    """拥有一个新目录、最多64行缓冲和三个实际文件,封口后只允许顺序回读。"""2122    def __init__(self,path,budget,*,check_cancel=None,buffer_rows=64,integrity=True):23        """预约并创建暂存。参数: path新目录、budget、取消回调及批行数。返回: owner。异常: 配置/预算/I/O错。24        """25        if type(buffer_rows) is not int or not 1<=buffer_rows<=64:26            raise ValueError('报文排序缓冲需要1至64行')27        if type(integrity) is not bool: raise TypeError("integrity需要bool")28        self.integrity=integrity29        self.path=Path(path); self._budget=budget; self.check_cancel=check_cancel30        self._held_cost=self._fixed_cost=self._buffer_cost=031        self.buffer_rows=buffer_rows; self.buffer=[]; self.handles=[]32        self._closed=False; self.state='building'; self.count=self.runs=033        self.level=0; self.secret=None; self.cursor=None; self.pending_file=None34        try:35            # 两路解码树/编码/缓冲及句柄均有固定工作空间,实际行缓冲另计。36            _reserve_owned_ram(self,6*1048576,'_fixed_cost')37            self.path.mkdir(parents=True,exist_ok=False); self.secret=token_bytes(32) if integrity else None38        except BaseException as error:39            _cleanup_owner(self,error,'frame_sort_cleanup_owner','报文排序构造')40            raise4142    def _check(self):43        """在有界I/O间检查取消。参数: 无。返回: None。异常: 关闭或取消。44        """45        if self._closed:46            raise ResultClosedError('报文排序已关闭')47        if self.check_cancel is not None:48            self.check_cancel()4950    def _open(self,path,mode):51        """登记真实句柄后交付。参数: path、模式。返回: 文件。异常: 打开或登记失败。52        """53        self.pending_file=path.open(mode,buffering=65536)54        stream=self.pending_file55        self.handles.append(stream); self.pending_file=None56        return stream5758    def _close_file(self,stream):59        """关闭成功才撤销登记。参数: stream实际流。返回: None。异常: 关闭错保责任。60        """61        stream.close(); self.handles.remove(stream)6263    def _mac(self,path,index,body):64        """把完整行绑定当前run及顺序。参数: path/index/body。返回: 64字节摘要。异常: 原类型错。65        """66        digest=blake2b(key=self.secret,digest_size=32)67        digest.update(path.name.encode('ascii')); digest.update(str(index).encode('ascii'))68        digest.update(b'\0'); digest.update(body)69        return digest.hexdigest().encode('ascii')7071    def _write(self,stream,index,packet):72        """有界编码并实写一行。参数: stream/index/packet。返回: None。异常: 配额/超行/写入错。73        """74        self._check()75        body=json.dumps(packet,ensure_ascii=False,allow_nan=False,separators=(',',':')).encode('utf-8')76        if len(body)>65536:77            raise ResultFormatError('报文排序单行超过64KiB')78        payload=(self._mac(Path(stream.name),index,body)+b' ' if self.integrity else b'')+body+b'\n'79        self._budget.reserve_spool(len(payload))80        position=081        while position<len(payload):82            written=stream.write(payload[position:])83            if type(written) is not int or not 0<written<=len(payload)-position:84                raise OSError('报文排序写入无进展')85            position+=written8687    def _read(self,path):88        """逐行核完整性,必须读到合法结束记录。参数: path本次run。返回: 记录生成器。异常: 改写/截断/关闭错。89        """90        stream=self._open(path,'rb'); index=0; primary=None91        try:92            while True:93                self._check(); raw=stream.readline(65604)94                if not raw.endswith(b'\n') or len(raw)>(65602 if self.integrity else 65537) or self.integrity and raw[64:65]!=b' ':95                    raise ResultFormatError('报文排序行截断或超限')96                body=raw[65:-1] if self.integrity else raw[:-1]97                if self.integrity and not compare_digest(raw[:64],self._mac(path,index,body)):98                    raise ResultFormatError('报文排序行内容或顺序被修改')99                packet=json.loads(body)100                if type(packet) is dict:101                    if packet!={'end':index} or stream.read(1):102                        raise ResultFormatError('报文排序结束记录不一致')103                    return104                if type(packet) is not list or len(packet)!=3 or type(packet[0]) is not list:105                    raise ResultFormatError('报文排序行结构非法')106                index+=1107                yield (tuple(packet[0]),packet[1],packet[2])108        except BaseException as error:109            primary=error110            raise111        finally:112            # 校验/取消是主错误;关闭错误只能附注,真实句柄保owner供再次close。113            try:114                self._close_file(stream)115            except BaseException as cleanup:116                if primary is None:117                    raise118                primary.add_note('报文排序回读关闭待恢复: '+repr(cleanup))119120    def append(self,key,packet):121        """预约后缓冲一条。参数: key有限同类型标量tuple、packet有限JSON对象。返回: None。异常: 结构/预算/I/O错。122        """123        self._check()124        if self.state!='building' or type(key) is not tuple or len(key)>32 or any(type(v) not in (int,str) for v in key):125            raise ValueError('报文排序需要building及有限标量键')126        encoded=json.dumps(packet,ensure_ascii=False,allow_nan=False,separators=(',',':')).encode('utf-8')127        if len(encoded)>65000:128            raise ResultFormatError('报文记录超过排序行能力')129        # JSON往返冻结调用者数据,避免追加后修改改变尚未写盘的结果。130        _reserve_owned_ram(self,4096+len(encoded)*32,'_buffer_cost')131        self.buffer.append((key,self.count,json.loads(encoded))); self.count+=1132        if len(self.buffer)>=self.buffer_rows:133            self._flush()134135    def _flush(self):136        """稳定排序有限缓冲并保存一个run。参数: 无。返回: None。异常: 排序/I/O/配额错。137        """138        if not self.buffer:139            return140        self.buffer.sort(key=lambda item:(item[0],item[1]))141        stream=self._open(self.path/f'0-{self.runs}.jsonl','xb')142        for index,packet in enumerate(self.buffer):143            self._write(stream,index,packet)144        self._write(stream,len(self.buffer),{'end':len(self.buffer)})145        self._close_file(stream); self.runs+=1; self.buffer.clear()146        _release_owned_ram(self,self._buffer_cost,'_buffer_cost')147148    def _merge(self,left,right,target):149        """只保两路当前行进行稳定归并。参数: 两个run及新目标。返回: None。异常: 输入校验/写入/关闭错。150        """151        first=self._read(left); second=self._read(right) if right is not None else iter(())152        stream=self._open(target,'xb'); count=0; primary=None153        try:154            a=next(first,None); b=next(second,None)155            while a is not None or b is not None:156                choose_first=b is None or a is not None and (a[0],a[1])<=(b[0],b[1])157                row=a if choose_first else b158                self._write(stream,count,row); count+=1159                if choose_first:160                    a=next(first,None)161                else:162                    b=next(second,None)163            self._write(stream,count,{'end':count}); self._close_file(stream)164        except BaseException as error:165            primary=error166            raise167        finally:168            cleanup_error=None169            for reader in (first,second):170                close=getattr(reader,'close',None)171                if close is not None:172                    try:173                        close()174                    except BaseException as cleanup:175                        cleanup_error=cleanup_error or cleanup176            if cleanup_error is not None:177                if primary is None:178                    raise cleanup_error179                primary.add_note('报文归并游标关闭待恢复: '+repr(cleanup_error))180181    def seal(self):182        """完成所有有界归并,空输入也有完整结束记录。参数: 无。返回: None。异常: 输入/预算/取消/I/O错。183        """184        self._check()185        if self.state!='building':186            raise ResultClosedError('报文排序不能重复封口')187        self._flush()188        if not self.runs:189            stream=self._open(self.path/'0-0.jsonl','xb')190            self._write(stream,0,{'end':0}); self._close_file(stream); self.runs=1191        while self.runs>1:192            for index in range(0,self.runs,2):193                self._merge(self.path/f'{self.level}-{index}.jsonl',194                    self.path/f'{self.level}-{index+1}.jsonl' if index+1<self.runs else None,195                    self.path/f'{self.level+1}-{index//2}.jsonl')196            self.level+=1; self.runs=(self.runs+1)//2197        self.state='sealed'198199    def rows(self):200        """交付单一可关闭回读游标。参数: 无。返回: (key,序号,记录)生成器。异常: 非sealed或已有游标。201        """202        self._check()203        if self.state!='sealed' or self.cursor is not None:204            raise ResultClosedError('报文排序未封口或已交付游标')205        self.cursor=self._read(self.path/f'{self.level}-0.jsonl')206        return self.cursor207208    def close(self):209        """关闭全部实际文件后释放自有预算;保暂存供诊断。参数: 无。返回: None。异常: 原关闭/释放错可重试。210        """211        if self._closed:212            return213        if self.cursor is not None:214            self.cursor.close(); self.cursor=None215        if self.pending_file is not None:216            self.pending_file.close(); self.pending_file=None217        while self.handles:218            self._close_file(self.handles[-1])219        self.buffer.clear(); self.secret=None220        _release_owned_ram(self,self._buffer_cost,'_buffer_cost')221        _release_owned_ram(self,self._fixed_cost,'_fixed_cost'); self._closed=True222
Chronaxis 用户手册 · 离线阅读