Repository navigation
feat/uploader thread split #1523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
b84c693
chore: add clean makefile
Nexisato 99b0407
feat: add lightweight timer scheduler
Nexisato e2d5613
fix: test_timer
Nexisato 1b1fe80
fix: task_triggered event in ut time
Nexisato 7139b1b
feat: draft upload thread
Nexisato d251a0d
refactor: upload with grpc record service
Nexisato 26d315d
refactor: add grpc server placeholder
Nexisato ff47729
chore: delet taskmd
Nexisato 55534c8
feat: add create core factory
Nexisato 34b9d57
refactor: reuse timer
Nexisato f44ad12
refactor: flattent uploader dir
Nexisato 730c67b
fix: rename uploader
Nexisato 2c6cd0b
fix: lock protection
Nexisato 1424359
feat: group records by type and add HTTP transport
Nexisato 5588647
feat: remove core grpc config
Nexisato db42837
refactor: uploader thread name
Nexisato 3a90618
feat: add try-catch pending for collector
Nexisato d23bc2b
refactor: put queue once
Nexisato 0c2a014
docs: add comments for test_timer
Nexisato 78bbccf
feat(test): add helper and sender test
Nexisato 8f11033
fix: rename test helper by module
Nexisato 0e3a5a1
fix(test): upload immediately for zero interval
Nexisato d65f53e
Fix test file header filename typo
SAKURA-CAT 7f60ef5
Update collector docstrings wording
SAKURA-CAT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #!/bin/bash | ||
| # Description: 递归清除指定目录下的 __pycache__ 和 .pyc 文件 | ||
| # 如果删除 __pycache__ 后导致其父目录为空,则连同由于删除产生的空父目录一起递归向上删除 | ||
|
|
||
| # 目标目录,默认当前目录 | ||
| TARGET_DIR="${1:-.}" | ||
|
|
||
| echo "开始清理 $TARGET_DIR 目录下的 Python 缓存文件..." | ||
|
|
||
| # 1. 寻找所有的 __pycache__ 文件夹,并循环处理 | ||
| # 注意:find 结果通过 while 读取,处理带有空格等特殊符号的路径 | ||
| find "$TARGET_DIR" -type d -name "__pycache__" | while read -r cache_dir; do | ||
| # 获取 __pycache__ 所在的父目录 | ||
| parent_dir="$(dirname "$cache_dir")" | ||
|
|
||
| # 删除该 __pycache__ 目录及里面的缓存文件 | ||
| rm -rf "$cache_dir" | ||
|
|
||
| echo "已清理: $cache_dir" | ||
|
|
||
| # 尝试递归往上删除空的父目录 | ||
| # rmdir 的特性是:只有在此目录没有任何文件/文件夹时才会删除并返回成功 | ||
| # 因此我们循环往上 rmdir,如果哪个父层级除了 __pycache__ 还有其它文件,自然会失败并跳出循环 | ||
| while [ "$parent_dir" != "." ] && [ "$parent_dir" != "/" ] && [ "$parent_dir" != "$TARGET_DIR" ]; do | ||
| if rmdir "$parent_dir" 2>/dev/null; then | ||
| echo "已删除原本仅含 __pycache__ 的空父目录: $parent_dir" | ||
| # 指向更上层父目录继续检查 | ||
| parent_dir="$(dirname "$parent_dir")" | ||
| else | ||
| # 遇到了非空目录(包含了其它文件),终止向上删除 | ||
| break | ||
| fi | ||
| done | ||
| done | ||
|
|
||
| # 2. 清理散落在其它地方的独立 .pyc 后缀缓存文件 | ||
| find "$TARGET_DIR" -type f -name "*.pyc" -delete 2>/dev/null | ||
|
|
||
| echo "清理完成!" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """ | ||
| @author: caddiesnew | ||
| @file: __init__.py | ||
| @time: 2026/3/19 | ||
| @description: 指标上传线程,基于 protobuf Record | ||
| """ | ||
|
|
||
| from .collector import Collector | ||
| from .sender import ( | ||
| HttpRecordTransport, | ||
| create_record_transport, | ||
| trace_records, | ||
| upload_records, | ||
| ) | ||
| from .thread import Uploader | ||
|
|
||
| __all__ = [ | ||
| "Collector", | ||
| "HttpRecordTransport", | ||
| "Uploader", | ||
| "create_record_transport", | ||
| "trace_records", | ||
| "upload_records", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| """ | ||
| @author: caddiesnew | ||
| @file: collector.py | ||
| @time: 2026/3/21 | ||
| @description: 上传线程采集器 | ||
| """ | ||
|
|
||
| import threading | ||
| import time | ||
| from typing import Callable, List, Optional | ||
|
|
||
| from swanlab.proto.swanlab.record.v1.record_pb2 import Record | ||
| from swanlab.sdk.internal.core_python.uploader.sender import upload_records | ||
| from swanlab.sdk.internal.pkg import console | ||
|
|
||
|
|
||
| class Collector: | ||
| """ | ||
| 日志采集器,负责从队列中收集 Records 并批量上传。 | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| upload_interval: float = 1.0, | ||
| upload_callback: Optional[Callable] = None, | ||
| ): | ||
| self.container: List[Record] = [] | ||
| self._lock = threading.Lock() | ||
| self._upload_interval = upload_interval | ||
| self._upload_callback = upload_callback | ||
| self._last_upload_at = time.time() | ||
|
|
||
| def upload(self, pending: List[Record]) -> None: | ||
| """ | ||
| 核心上传逻辑:直接批量上送 protobuf Record。 | ||
| 由于网络请求耗时,应在无锁状态下调用。 | ||
| """ | ||
| if not pending: | ||
| return | ||
| upload_records(pending, upload_callback=self._upload_callback) | ||
|
|
||
| def submit(self, records: List[Record]) -> None: | ||
| """提交记录,由定时线程以 upload_interval 为周期进行调用""" | ||
| with self._lock: | ||
| self.container.extend(records) | ||
| if not self.container: | ||
| return | ||
|
|
||
| pending = self.container | ||
| if self._upload_interval <= 0: | ||
| self.container = [] | ||
| else: | ||
| now = time.time() | ||
| if now - self._last_upload_at <= self._upload_interval: | ||
| return | ||
|
|
||
| self._last_upload_at = now | ||
| self.container = [] | ||
|
|
||
| try: | ||
| self.upload(pending) | ||
| except Exception as exc: | ||
| console.error(f"upload error: {exc}") | ||
| with self._lock: | ||
| self.container = pending + self.container | ||
|
|
||
| def flush(self, records: List[Record]) -> None: | ||
| """刷新所有累积的 records 进行上传""" | ||
| with self._lock: | ||
| self.container.extend(records) | ||
| pending = self.container | ||
| self.container = [] | ||
|
|
||
| try: | ||
| self.upload(pending) | ||
| except Exception as exc: | ||
| console.error(f"upload error: {exc}") | ||
| with self._lock: | ||
| self.container = pending + self.container | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "Collector", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #!/usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
| r""" | ||
| @author: caddiesnew | ||
| @file: helper.py | ||
| @time: 2026/3/30 15:33 | ||
| @description: uploader 辅助函数 | ||
| """ | ||
|
|
||
| from collections import OrderedDict | ||
| from typing import Dict, Iterator, List, Sequence, Tuple | ||
|
|
||
| from swanlab.proto.swanlab.record.v1.record_pb2 import Record | ||
|
|
||
|
|
||
| def generate_chunks(records: Sequence[Record], per_request_len: int) -> Iterator[Tuple[Sequence[Record], int]]: | ||
| """ | ||
| 按固定大小对 Record 序列做切片。 | ||
| yield: (分片数据, 分片长度) | ||
| """ | ||
| if per_request_len == -1: | ||
| yield records, len(records) | ||
| return | ||
| if per_request_len <= 0: | ||
| raise ValueError(f"per_request_len must be -1 or > 0, got {per_request_len}") | ||
|
|
||
| total = len(records) | ||
| if total <= per_request_len: | ||
| yield records, total | ||
| return | ||
|
|
||
| for index in range(0, total, per_request_len): | ||
| chunk = records[index : index + per_request_len] | ||
| yield chunk, len(chunk) | ||
|
|
||
|
|
||
| def group_records_by_type(records: Sequence[Record]) -> Dict[str, List[Record]]: | ||
| """按照 record 的类型进行分组""" | ||
| grouped: Dict[str, List[Record]] = OrderedDict() | ||
| for record in records: | ||
| if not isinstance(record, Record): | ||
| raise TypeError(f"group_records_by_type only accepts Record instances, got {type(record).__name__}") | ||
| record_type = record.WhichOneof("record_type") | ||
| if record_type is None: | ||
| raise ValueError("Record has no active record_type and cannot be uploaded.") | ||
| grouped.setdefault(record_type, []).append(record) | ||
| return grouped |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| """ | ||
| @author: caddiesnew | ||
| @file: sender.py | ||
| @time: 2026/3/19 | ||
| @description: Record 上传入口与 Core sidecar transport | ||
| """ | ||
|
|
||
| from typing import Callable, Optional, Sequence | ||
|
|
||
| from swanlab.proto.swanlab.record.v1.record_pb2 import Record | ||
| from swanlab.sdk.internal.pkg import console | ||
|
|
||
| from .helper import generate_chunks, group_records_by_type | ||
|
|
||
|
|
||
| class HttpRecordTransport: | ||
| """PLACEHOLDER: 基于现有 HTTP client 的上传 transport 。""" | ||
|
|
||
| def upload_record_group(self, record_type: str, records: Sequence[Record]) -> None: | ||
| if len(records) == 0: | ||
| return | ||
| console.debug(f"HTTP upload skeleton is ready for record_type={record_type!r}, but request mapping is pending.") | ||
|
|
||
| def close(self) -> None: | ||
| pass | ||
|
|
||
|
|
||
| def create_record_transport() -> HttpRecordTransport: | ||
| """创建 Record transport。""" | ||
| return HttpRecordTransport() | ||
|
|
||
|
|
||
| def trace_records( | ||
| records: Optional[Sequence[Record]] = None, | ||
| per_request_len: int = 10_000, | ||
| upload_callback: Optional[Callable[[int], None]] = None, | ||
| transport: Optional[HttpRecordTransport] = None, | ||
| ) -> None: | ||
| """ | ||
| 分片上传 protobuf Record。 | ||
|
|
||
| 线程层负责缓冲、聚合与重试;此层只做 Record 分片、按 record_type 分组和 transport 调用。 | ||
| """ | ||
| if records is None or len(records) == 0: | ||
| return | ||
|
|
||
| owns_transport = transport is None | ||
| active_transport = transport or create_record_transport() | ||
|
|
||
| try: | ||
| for chunk, chunk_len in generate_chunks(records, per_request_len): | ||
| for record_type, grouped_records in group_records_by_type(chunk).items(): | ||
| active_transport.upload_record_group(record_type, grouped_records) | ||
| if upload_callback: | ||
| upload_callback(chunk_len) | ||
| finally: | ||
| if owns_transport: | ||
| active_transport.close() | ||
|
|
||
|
|
||
| def upload_records( | ||
| records: Sequence[Record], | ||
| upload_callback: Optional[Callable[[int], None]] = None, | ||
| per_request_len: int = 10_000, | ||
| ) -> None: | ||
| """上传一组 protobuf Record,不在此层做任何 JSON 化。""" | ||
| if len(records) == 0: | ||
| return console.debug("No records to upload.") | ||
| trace_records(records, per_request_len=per_request_len, upload_callback=upload_callback) | ||
| return None | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "HttpRecordTransport", | ||
| "create_record_transport", | ||
| "trace_records", | ||
| "upload_records", | ||
| ] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.