modify: worker 管理多进程副本
This commit is contained in:
@@ -238,7 +238,7 @@ worker/<type>/<worker_id>/__admin__
|
|||||||
| `restart` | 重启该副本,副本槽位(index)由管理循环驱动,重新生成新 `worker_id`(`worker-<index>-<epoch>-<pid>`)并经 liveliness 更新 |
|
| `restart` | 重启该副本,副本槽位(index)由管理循环驱动,重新生成新 `worker_id`(`worker-<index>-<epoch>-<pid>`)并经 liveliness 更新 |
|
||||||
| `stop` | 停止该副本 |
|
| `stop` | 停止该副本 |
|
||||||
| `drain` | **优雅排空**:摘除 liveliness/业务 Queryable 隔绝新请求,等待正在处理的在途请求完成后再退出,管理循环不重建(数量-1) |
|
| `drain` | **优雅排空**:摘除 liveliness/业务 Queryable 隔绝新请求,等待正在处理的在途请求完成后再退出,管理循环不重建(数量-1) |
|
||||||
| `rescale` | 按 `data.scale` 把**当前进程内**该类型副本数调整为指定值(`data:{"scale":N}`),并把 `[workers].count` 写回本进程侧配置 |
|
| `rescale` | 按 `data.scale` 把**当前进程内**该类型副本数调整为指定值(`data:{"scale":N}`);**两种口径**:不带 `instance_id` 时为「全局对称分摊」(写回全局 count + 各实例均分),带 `instance_id + absolute=true` 且命中本实例时为「逐实例精细扩缩」(只改该实例绝对副本数),target 非本实例则安全忽略 |
|
||||||
| `config_set`| 更新 `[workers.config]` 运行时配置(路由配置除外);成功则写回配置并重启该类型全部对应副本,任一失败返回失败信息 |
|
| `config_set`| 更新 `[workers.config]` 运行时配置(路由配置除外);成功则写回配置并重启该类型全部对应副本,任一失败返回失败信息 |
|
||||||
|
|
||||||
### REST 管理端点(仅超管)
|
### REST 管理端点(仅超管)
|
||||||
@@ -250,7 +250,9 @@ worker/<type>/<worker_id>/__admin__
|
|||||||
| `POST` | `/api/v1/admin/workers/<type>/stop` | 停止 single 副本 / 某插件全部副本 |
|
| `POST` | `/api/v1/admin/workers/<type>/stop` | 停止 single 副本 / 某插件全部副本 |
|
||||||
| `GET` | `/api/v1/admin/workers/<type>/config` | 读取该插件的现有 `[workers.config]`(展示当前值) |
|
| `GET` | `/api/v1/admin/workers/<type>/config` | 读取该插件的现有 `[workers.config]`(展示当前值) |
|
||||||
| `POST` | `/api/v1/admin/workers/<type>/config` | 更新配置(body `{updates:{k:v}}`),下发并重启 |
|
| `POST` | `/api/v1/admin/workers/<type>/config` | 更新配置(body `{updates:{k:v}}`),下发并重启 |
|
||||||
| `POST` | `/api/v1/admin/workers/<type>/scale` | 调整该类型副本数(body `{count:N}`),扩缩容 + 持久化 |
|
| `POST` | `/api/v1/admin/workers/<type>/scale` | 调整该类型副本数(body `{count:<全局总量>}`):写全局 count → 清除逐实例定制回均分 → 广播全部存活实例,**全局对称分摊**
|
||||||
|
| `GET` | `/api/v1/admin/workers/<type>/instances` | 按外部进程实例(instance_id)分组读取该类型副本(供前端逐实例精细扩缩展示) |
|
||||||
|
| `POST` | `/api/v1/admin/workers/<type>/scale_instance` | **逐实例精细扩缩**(body `{instance_id, count}`):只把指定实例的副本数调到 N,其余实例不动,持久化到 `instcounts` 覆盖表 |
|
||||||
|
|
||||||
> 旧版运行时未声明 `__admin__` 管理能力时,管理指令会超时(HTTP 502,见日志提示)。
|
> 旧版运行时未声明 `__admin__` 管理能力时,管理指令会超时(HTTP 502,见日志提示)。
|
||||||
|
|
||||||
@@ -267,7 +269,8 @@ worker/<type>/<worker_id>/__admin__
|
|||||||
- **扩容**:新增副本槽位(`WorkerPool` 新增 `_managed_worker` 管理循环,新副本注册 liveliness 后可被负载均衡)。
|
- **扩容**:新增副本槽位(`WorkerPool` 新增 `_managed_worker` 管理循环,新副本注册 liveliness 后可被负载均衡)。
|
||||||
- **缩容**:随机挑选多余副本触发 **`drain`(优雅排空)**——先摘除 liveliness/业务 Queryable **隔绝新请求**,等待正在处理的**在途请求完成**后再关闭该副本(`in-flight` 计数实现),避免中断业务;管理循环不重建。
|
- **缩容**:随机挑选多余副本触发 **`drain`(优雅排空)**——先摘除 liveliness/业务 Queryable **隔绝新请求**,等待正在处理的**在途请求完成**后再关闭该副本(`in-flight` 计数实现),避免中断业务;管理循环不重建。
|
||||||
- **目标数量持久化**:`rescale` 把 `[workers].count` 写回 `config.toml`,网关/进程重启后按新数量加载。
|
- **目标数量持久化**:`rescale` 把 `[workers].count` 写回 `config.toml`,网关/进程重启后按新数量加载。
|
||||||
- **统一协议**:inline 与 external 共用。网关对 inline 直接调整本进程 `WorkerPool`;对 external 经 Zenoh 向任一存活副本下发 `rescale`,由其**所在进程内**的池扩缩并写回自身配置(无需重启整个进程/容器)。
|
- **统一协议**:inline 与 external 共用。网关对 inline 直接调整本进程 `WorkerPool`;对 external 经 Zenoh **向该类型全部存活实例广播 `rescale`**(不只是挑一个副本),由各实例按其 quota 扩缩并写回自身配置(无需重启整个进程/容器)。
|
||||||
|
- **逐实例 vs 全局**:`/scale` 是对某个类型的**全局对称扩缩**;若外部进程有多个(多机/多容器),`/scale` 会把全局总量均分到每个存活实例。要单独调整**某一个**外部实例的副本数,见下文「外部进程实例粒度扩缩」。
|
||||||
|
|
||||||
### 全局副本数协调(多网关)
|
### 全局副本数协调(多网关)
|
||||||
|
|
||||||
@@ -286,7 +289,21 @@ worker/<type>/<worker_id>/__admin__
|
|||||||
launch_worker_count = "toml" # "toml"(默认)=启动用 toml 的 count 覆盖并写回 cache;"cache"=cache 为单一事实来源,toml 仅在缺失时引导
|
launch_worker_count = "toml" # "toml"(默认)=启动用 toml 的 count 覆盖并写回 cache;"cache"=cache 为单一事实来源,toml 仅在缺失时引导
|
||||||
```
|
```
|
||||||
|
|
||||||
- **进程插件(external)不受该开关影响**:网关只对 **inline** 插件用 cache 解析启动副本数(见「启动流程」第 5 步 `load_workers`);external 插件由外部编排按其自身配置 `config/workers/<type>/config.toml` 的 `count` 启动,不经网关、也不读 cache。运行时 `scale` 对 external 仍走「向存活副本下发 rescale」路径,同样与该开关无关。
|
- **进程插件(external)实例粒度参与分摊**:网关只对 **inline** 插件用 cache 解析启动副本数(见「启动流程」第 5 步 `load_workers`),external 插件的启动副本数由外部编排按自身配置决定;但**运行时的缩放**上,external 并不退化为“每进程全额”,而是同样在**共享 cache 上登记实例参与方**来分摊,避免多实例把副本翻倍:
|
||||||
|
- **实例登记 + 心跳租约**:每个 external 进程启动时向 `worker:cm:instances:<type>` 登记自身参与方(`[instance_id, ts]`),并每 15s 心跳续约;超过租约(60s)未心跳的死实例会被清理。`divmod` 按存活参与方 N 均分全局 count(余量补靠前实例),`N==1` 时退化为全额(单部署零影响)。
|
||||||
|
- **实例标识默认唯一**:`--instance` / 环境变量 `CHONGMING_WORKER_INSTANCE` 显式指定(跨重启稳定、逐实例精细扩缩可持续命中);缺省回退 `{hostname}:{pid}`——**同一机器并行多个未指定 `--instance` 的进程也能靠 pid 区分**,不会因为 id 相同被并成一个参与方导致副本翻倍或前端只显示一个实例。注意缺省 id 随进程重启变化。
|
||||||
|
- **广播 + 幂等收敛**:`/scale` 写全局 count 后向该类型**全部存活实例**广播 `rescale`,各实例读同一全局 count + 同一份参与方列表算出的 quota 一致,重复 rescale 也幂等,因此任意地收敛到同一预期状态。
|
||||||
|
- **前提**:external 需共用共享 `[cache]`(多实例 redis 或共享卷 diskcache);`cache is None` 时退化为全额(仅单实例安全)。
|
||||||
|
|
||||||
|
### 外部进程实例粒度扩缩(逐实例精细扩缩)
|
||||||
|
|
||||||
|
当同一个类型跑多个外部进程(多机/多容器)时,除了 `scale` 的**全局对称扩缩**,还可以只对**某一个实例**精确扩缩:
|
||||||
|
|
||||||
|
- **语义**:把目标实例的副本数**绝对值**设为 N,其它实例不动;与全局 `scale` 共存——全局对称是默认/一键复位,逐实例是定制覆盖。
|
||||||
|
- **覆盖表**:每个实例的绝对目标存 cache 键 `worker:cm:instcounts:<type>` = `{instance_id: 绝对副本数}`。有记录的实例按绝对数执行;没记录的实例回退到全局均分。一旦用 `scale_instance` 定制了某实例,之后 `scale`(全局)会**清除整张覆盖表**回到均分(显式“复位”),因此两者不会互相干扰。
|
||||||
|
- **命中判断**:rescale 的下发必须命中**本实例**才执行(`instance_id` 与 `register_instance` 一致),目标是其它实例则安全忽略,避免误缩。
|
||||||
|
- **REST / UI**:`POST /admin/workers/<type>/scale_instance`(body `{instance_id, count}`)下发;`GET /admin/workers/<type>/instances` 供前端按实例分组展示当前副本数。前端用「调整数量」(全局对称)和「逐实例精细扩缩」表格(实例 id / 模式 / 当前副本数 / 目标 + 应用)两个入口。
|
||||||
|
- **协调前提**:与外部广播缩放一致,需共享 `[cache]`,且外部进程用稳定的 `--instance` / `CHONGMING_WORKER_INSTANCE`(缺省 `{hostname}:{pid}` 虽唯一但重启会变,跨重启持续命中请显式指定)。
|
||||||
|
|
||||||
### inline worker 宿主级领导选举(gunicorn 多 worker 兼容)
|
### inline worker 宿主级领导选举(gunicorn 多 worker 兼容)
|
||||||
|
|
||||||
@@ -314,7 +331,10 @@ inline worker 的生命周期交给**基于分布式锁的宿主级领导选举*
|
|||||||
- **登录**:经 auth 插件 `POST /auth/login` 领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。
|
- **登录**:经 auth 插件 `POST /auth/login` 领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。
|
||||||
- **鉴权**:仅 `is_superuser=true` 可管理;权限不足时前端/后端弹出对应提示(403)。
|
- **鉴权**:仅 `is_superuser=true` 可管理;权限不足时前端/后端弹出对应提示(403)。
|
||||||
- **列表分页**:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
|
- **列表分页**:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
|
||||||
- **操作**:单副本重启、批量重启(选中)、插件级重启全部副本、更新插件配置(弹窗展示现有配置值,点行带入编辑)、**调整副本数量**(当前数 + `el-input-number` 设目标 + 应用,可扩缩容)。
|
- **查询与筛选**:列表顶部统一筛选栏——关键字搜索(插件类型 / 副本 ID / 实例 ID)、模式筛选(全部 / inline / external)、副本数 ≥ N 过滤;**同时作用于下方「逐实例精细扩缩」分组表**(副本数过滤仅对分组表生效),筛选条件变化自动回到第一页。
|
||||||
|
- **操作**:单副本重启、批量重启(选中)、插件级重启全部副本、更新插件配置(弹窗展示现有配置值,点行带入编辑;支持对象/数组等嵌套值的**结构化 JSON 编辑与校验/格式化**)、**调整副本数量**(当前数 + `el-input-number` 设目标 + 应用,可扩缩容)。
|
||||||
|
- **逐实例精细扩缩**:按外部进程实例(instance_id)**分组**展示(实例 id / 模式 / 当前副本数),每行可设目标副本数并「应用」——仅调整该实例,其余实例不动;全局「调整数量」后分组表也会自动刷新。
|
||||||
|
- **自动刷新**:页面每 10s 轮询总副本列表**与**逐实例分组表,扩缩容/重启/停止等异步结果无需手动刷新即可反映。
|
||||||
- **auth 核心插件**:配置不可更改,不在列表中展示(`Worker` 列表自动过滤 `worker_type == "auth"`)。
|
- **auth 核心插件**:配置不可更改,不在列表中展示(`Worker` 列表自动过滤 `worker_type == "auth"`)。
|
||||||
- 调小副本数会**触发优雅排空**并在前端确认提示中说明。
|
- 调小副本数会**触发优雅排空**并在前端确认提示中说明。
|
||||||
|
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
|
|||||||
factory_name = wc["factory"]
|
factory_name = wc["factory"]
|
||||||
# count 以 chongming-cache 为单一事实来源:cache 有值时用 cache 的全局总量,
|
# count 以 chongming-cache 为单一事实来源:cache 有值时用 cache 的全局总量,
|
||||||
# 否则用配置值引导并写回 cache(多网关共享同一 cache 时自动对齐)。
|
# 否则用配置值引导并写回 cache(多网关共享同一 cache 时自动对齐)。
|
||||||
count = manager.resolved_count(wc["type"], wc["count"])
|
count = wc["count"]
|
||||||
worker_type = wc["type"]
|
worker_type = wc["type"]
|
||||||
worker_config = wc["config"]
|
worker_config = wc["config"]
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import random
|
|||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
import orjson
|
import orjson
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional
|
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional
|
||||||
import zenoh
|
import zenoh
|
||||||
from chongming_cache import Cache, DistributedLock
|
from chongming_cache import Cache, DistributedLock
|
||||||
@@ -16,9 +17,7 @@ logger = get_logger("worker_manager")
|
|||||||
# ── 全局副本数(chongming-cache 单一事实来源)──
|
# ── 全局副本数(chongming-cache 单一事实来源)──
|
||||||
# 多网关共用同一 cache(单实例=本地 diskcache,多实例=共享卷/redis),
|
# 多网关共用同一 cache(单实例=本地 diskcache,多实例=共享卷/redis),
|
||||||
# 「count」语义 = 该类 worker 的**全局目标总量**,各网关按在线数量分摊。
|
# 「count」语义 = 该类 worker 的**全局目标总量**,各网关按在线数量分摊。
|
||||||
_SCALE_DESIRED_KEY = "worker:cm:counts:{type}" # 全局目标总量(int)
|
|
||||||
_SCALE_GATEWAYS_KEY = "worker:cm:gateways" # 在线网关列表:[[id, 最近心跳 ts], ...](JSON)
|
_SCALE_GATEWAYS_KEY = "worker:cm:gateways" # 在线网关列表:[[id, 最近心跳 ts], ...](JSON)
|
||||||
_SCALE_QUOTA_KEY = "worker:cm:quota:{type}:{gw}" # 各网关分摊配额(int)
|
|
||||||
|
|
||||||
# 在线网关租约(秒):网关心跳每 refresh_interval(15s) 更新一次自身 ts,超过 LEASE 未刷新
|
# 在线网关租约(秒):网关心跳每 refresh_interval(15s) 更新一次自身 ts,超过 LEASE 未刷新
|
||||||
# 视为离线的死网关,在下一次读写时被清理。容忍多次漏跳,且 crash/kill -9 不残留。
|
# 视为离线的死网关,在下一次读写时被清理。容忍多次漏跳,且 crash/kill -9 不残留。
|
||||||
@@ -52,6 +51,38 @@ def _next_reply(iterator):
|
|||||||
return _SENTINEL
|
return _SENTINEL
|
||||||
|
|
||||||
|
|
||||||
|
class _MemLRU:
|
||||||
|
"""极简进程内一级缓存:LRU + TTL。
|
||||||
|
|
||||||
|
仅度热路径(cacheable RPC 命中)/JWT 等高频只读复用。单事件循环内读写、
|
||||||
|
OrderedDict 实现,O(1) 无锁;作为 diskcache/redis 二级共享缓存的进程级第一级,
|
||||||
|
命中时完全绕过网络/磁盘与 Zenoh,降低热点请求延迟与后端压力。
|
||||||
|
maxsize 达到上限时淘汰最久未用(LRU)条目;TTL 削防陈旧数据长期驻留。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, maxsize: int = 2048, ttl: float = 60.0):
|
||||||
|
self._d: "OrderedDict[str, tuple]" = OrderedDict()
|
||||||
|
self._maxsize = maxsize
|
||||||
|
self._ttl = ttl
|
||||||
|
|
||||||
|
def get(self, key: str):
|
||||||
|
item = self._d.get(key)
|
||||||
|
if item is None:
|
||||||
|
return None
|
||||||
|
val, expire = item
|
||||||
|
if expire < time.monotonic():
|
||||||
|
self._d.pop(key, None)
|
||||||
|
return None
|
||||||
|
self._d.move_to_end(key) # 更新为最近使用
|
||||||
|
return val
|
||||||
|
|
||||||
|
def set(self, key: str, val) -> None:
|
||||||
|
self._d[key] = (val, time.monotonic() + self._ttl)
|
||||||
|
self._d.move_to_end(key)
|
||||||
|
if len(self._d) > self._maxsize:
|
||||||
|
self._d.popitem(last=False)
|
||||||
|
|
||||||
|
|
||||||
class WorkerManager:
|
class WorkerManager:
|
||||||
"""
|
"""
|
||||||
统一 Worker 管理器
|
统一 Worker 管理器
|
||||||
@@ -68,6 +99,9 @@ class WorkerManager:
|
|||||||
launch_worker_count: str = "toml"):
|
launch_worker_count: str = "toml"):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
# RPC 缓存进程内一级(LRU+TTL),兜底并加速 cacheable 命中;
|
||||||
|
# 二级为共享 cache(diskcache/redis),I/O 移出事件循环走 executor。
|
||||||
|
self._rpc_mem_cache = _MemLRU()
|
||||||
# 启动时全局副本数的优先来源:"toml"=用 toml 的 count 覆盖 cache(经典模式);
|
# 启动时全局副本数的优先来源:"toml"=用 toml 的 count 覆盖 cache(经典模式);
|
||||||
# "cache"=以 cache 为单一事实来源。对应 config.toml [cache].launch_worker_count。
|
# "cache"=以 cache 为单一事实来源。对应 config.toml [cache].launch_worker_count。
|
||||||
self._launch_worker_count = launch_worker_count
|
self._launch_worker_count = launch_worker_count
|
||||||
@@ -90,12 +124,8 @@ class WorkerManager:
|
|||||||
self._started_extra: List[Any] = []
|
self._started_extra: List[Any] = []
|
||||||
|
|
||||||
# ── 全局副本数协调(chongming-cache 单一事实来源)────────────
|
# ── 全局副本数协调(chongming-cache 单一事实来源)────────────
|
||||||
|
# count/quota 的读写与分摊已收归 worker 层(chongming_worker.runtime):
|
||||||
def _scale_desired_key(self, worker_type: str) -> str:
|
# 网关不再直写这些缓存键,仅维护「在线网关列表」(register/heartbeat/unregister)。
|
||||||
return _SCALE_DESIRED_KEY.format(type=worker_type)
|
|
||||||
|
|
||||||
def _scale_quota_key(self, worker_type: str) -> str:
|
|
||||||
return _SCALE_QUOTA_KEY.format(type=worker_type, gw=self._gateway_id)
|
|
||||||
|
|
||||||
def _read_gateways_raw(self) -> List[List]:
|
def _read_gateways_raw(self) -> List[List]:
|
||||||
"""读取在线网关原始列表 [[id, ts], ...]。"""
|
"""读取在线网关原始列表 [[id, ts], ...]。"""
|
||||||
@@ -249,44 +279,6 @@ class WorkerManager:
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error("[heal] 拉起 %s 失败: %s", wtype, e)
|
logger.error("[heal] 拉起 %s 失败: %s", wtype, e)
|
||||||
|
|
||||||
def resolved_count(self, worker_type: str, default: int) -> int:
|
|
||||||
"""获取该类 worker 的全局目标总量。
|
|
||||||
|
|
||||||
依 [cache].launch_worker_count 决定启动优先级:
|
|
||||||
- "toml"(经典):始终用 toml 的 count(default)覆盖 cache 并写回为新的全局总量;
|
|
||||||
- "cache"(全局):cache 为单一事实来源,缺失时才用 toml 引导并写回。
|
|
||||||
"""
|
|
||||||
if self.cache is None:
|
|
||||||
return int(default)
|
|
||||||
key = self._scale_desired_key(worker_type)
|
|
||||||
total = max(0, int(default))
|
|
||||||
if self._launch_worker_count == "toml":
|
|
||||||
# 经典模式:toml count 覆盖并写回 cache
|
|
||||||
self.cache[key] = total
|
|
||||||
return total
|
|
||||||
if key in self.cache:
|
|
||||||
return int(self.cache[key])
|
|
||||||
self.cache[key] = total
|
|
||||||
return total
|
|
||||||
|
|
||||||
def own_quota(self, worker_type: str, total: int) -> int:
|
|
||||||
"""在「count=全局总量」语义下,计算当前网关应承担的副本数(分摊)。
|
|
||||||
|
|
||||||
单网关(本场景)即总量本身;多网关按在线 id 排序均分,余量按序补前若干。
|
|
||||||
结果写回 cache(observability),并在全局下发给各网关后各自按其 quota 调整。
|
|
||||||
"""
|
|
||||||
total = max(0, int(total))
|
|
||||||
gws = sorted(self._gateways())
|
|
||||||
if not gws:
|
|
||||||
quota = total
|
|
||||||
else:
|
|
||||||
base, rem = divmod(total, len(gws))
|
|
||||||
idx = gws.index(self._gateway_id) if self._gateway_id in gws else 0
|
|
||||||
quota = base + (1 if idx < rem else 0)
|
|
||||||
if self.cache is not None:
|
|
||||||
self.cache[self._scale_quota_key(worker_type)] = quota
|
|
||||||
return quota
|
|
||||||
|
|
||||||
def register_worker(self, worker_type: str, worker_id: str):
|
def register_worker(self, worker_type: str, worker_id: str):
|
||||||
"""
|
"""
|
||||||
直接注册一个本地 worker(同进程内使用)
|
直接注册一个本地 worker(同进程内使用)
|
||||||
@@ -511,6 +503,36 @@ class WorkerManager:
|
|||||||
|
|
||||||
return _stream()
|
return _stream()
|
||||||
|
|
||||||
|
# ── 共享缓存二级 I/O(executor 化,不阻塞事件循环)────────────
|
||||||
|
# 进程内一级(_rpc_mem_cache)在事件循环内 O(1) 直读;二级 diskcache/redis
|
||||||
|
# 的 get/set 是同步 I/O,统一丢到 _ZENOH_EXECUTOR 线程执行,避免高并发下
|
||||||
|
# 磁盘/网络 I/O 卡死事件循环。值恒为 bytes,选 None 作为未命中哨兵安全。
|
||||||
|
|
||||||
|
async def _cache_get(self, key: str) -> Optional[bytes]:
|
||||||
|
if self.cache is None:
|
||||||
|
return None
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
return await loop.run_in_executor(_ZENOH_EXECUTOR, self._sync_cache_get, key)
|
||||||
|
|
||||||
|
def _sync_cache_get(self, key: str) -> Optional[bytes]:
|
||||||
|
try:
|
||||||
|
return self.cache.get(key) if self.cache is not None else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _cache_set(self, key: str, value: bytes) -> None:
|
||||||
|
if self.cache is None:
|
||||||
|
return
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
await loop.run_in_executor(_ZENOH_EXECUTOR, self._sync_cache_set, key, value)
|
||||||
|
|
||||||
|
def _sync_cache_set(self, key: str, value: bytes) -> None:
|
||||||
|
try:
|
||||||
|
if self.cache is not None:
|
||||||
|
self.cache[key] = value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
async def call(
|
async def call(
|
||||||
self,
|
self,
|
||||||
worker_type: str,
|
worker_type: str,
|
||||||
@@ -598,10 +620,19 @@ class WorkerManager:
|
|||||||
call() / call_raw() 共用本实现,仅入参出参形态不同(dict ↔ bytes)。
|
call() / call_raw() 共用本实现,仅入参出参形态不同(dict ↔ bytes)。
|
||||||
"""
|
"""
|
||||||
# 1. 缓存检查(仅在 cacheable=True 时检查)
|
# 1. 缓存检查(仅在 cacheable=True 时检查)
|
||||||
if cacheable and cache_key is not None and self.cache is not None:
|
if cacheable and cache_key is not None:
|
||||||
if cache_key in self.cache:
|
# 一级:进程内内存 LRU(事件循环内 O(1),零 I/O,命中完全绕过 Zenoh)
|
||||||
logger.debug(f"命中缓存: {cache_key}")
|
mem_val = self._rpc_mem_cache.get(cache_key)
|
||||||
return self.cache[cache_key]
|
if mem_val is not None:
|
||||||
|
logger.debug(f"命中内存缓存: {cache_key}")
|
||||||
|
return mem_val
|
||||||
|
# 二级:共享缓存(diskcache/redis),I/O 移到 executor,不阻塞事件循环
|
||||||
|
if self.cache is not None:
|
||||||
|
disk_val = await self._cache_get(cache_key)
|
||||||
|
if disk_val is not None:
|
||||||
|
self._rpc_mem_cache.set(cache_key, disk_val)
|
||||||
|
logger.debug(f"命中共享缓存: {cache_key}")
|
||||||
|
return disk_val
|
||||||
|
|
||||||
# 2. 获取可用 worker
|
# 2. 获取可用 worker
|
||||||
worker_expr = await self.get_worker(worker_type)
|
worker_expr = await self.get_worker(worker_type)
|
||||||
@@ -654,11 +685,13 @@ class WorkerManager:
|
|||||||
if reply.ok: # type: ignore
|
if reply.ok: # type: ignore
|
||||||
reply_bytes = bytes(reply.ok.payload) if reply.ok.payload else b"{}"
|
reply_bytes = bytes(reply.ok.payload) if reply.ok.payload else b"{}"
|
||||||
# 缓存写入(仅在 cacheable=True 时):解析一次做成功检查,
|
# 缓存写入(仅在 cacheable=True 时):解析一次做成功检查,
|
||||||
# 但缓存与返回均保留原始字节(网关侧零二次序列化)
|
# 但缓存与返回均保留原始字节(网关侧零二次序列化)。
|
||||||
if cacheable and cache_key is not None and self.cache is not None and reply_bytes:
|
# 先写内存一级,再异步写共享二级(executor,不阻塞响应返回)
|
||||||
|
if cacheable and cache_key is not None and reply_bytes:
|
||||||
parsed = orjson.loads(reply_bytes)
|
parsed = orjson.loads(reply_bytes)
|
||||||
if self.is_success_response(parsed):
|
if self.is_success_response(parsed):
|
||||||
self.cache[cache_key] = reply_bytes
|
self._rpc_mem_cache.set(cache_key, reply_bytes)
|
||||||
|
await self._cache_set(cache_key, reply_bytes)
|
||||||
return reply_bytes
|
return reply_bytes
|
||||||
else:
|
else:
|
||||||
error_payload = reply.err.payload # type: ignore
|
error_payload = reply.err.payload # type: ignore
|
||||||
@@ -824,59 +857,168 @@ class WorkerManager:
|
|||||||
"""把某类型的副本数调整为 count(扩缩容)并持久化到配置。
|
"""把某类型的副本数调整为 count(扩缩容)并持久化到配置。
|
||||||
|
|
||||||
下发逻辑(与 inline/external 的统一管理协议一致):
|
下发逻辑(与 inline/external 的统一管理协议一致):
|
||||||
- inline:类型在本进程内有 WorkerPool,可直接调用其 set_target(进程内扩缩,
|
- inline:类型在本进程内有 WorkerPool,只下发本网关进程内的池(进程内扩缩,
|
||||||
无需经过 Zenoh 往返);
|
无需跨进程广播,副本按其所在网关分摊配额);
|
||||||
- external:类型由独立进程/容器运行,向该类型任一存活副本经 Zenoh 下发
|
- external:类型由独立进程/容器运行,向该类型**全部存活**副本经 Zenoh 广播
|
||||||
rescale 指令,由其**所在进程内**的 WorkerPool 扩缩并写回自身配置。
|
rescale 指令,由各进程内 WorkerPool 按「全局 count ÷ 存活实例数」重新分摊
|
||||||
|
并各写回自身配置(避免只缩/扩其中一个实例造成不一致)。
|
||||||
|
全局缩放同时清除各实例的「逐实例绝对副本数」覆盖,回到对称分摊——因此它是
|
||||||
|
「一键复位对称」入口;逐实例精细缩放走 scale_instance(见后)。
|
||||||
|
|
||||||
缩容走优雅排空:摘除 liveliness/业务 Queryable 隔绝新请求,等正在处理的
|
缩容走优雅排空:摘除 liveliness/业务 Queryable 隔绝新请求,等正在处理的
|
||||||
在途请求完成后再关闭副本(避免中断业务)。
|
在途请求完成后再关闭副本(避免中断业务)。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
worker_type: 插件类型
|
worker_type: 插件类型
|
||||||
count: 目标副本数(>= 0)
|
count: 目标副本总数(全局总量,>= 0)
|
||||||
timeout: 下发超时(秒)
|
timeout: 下发超时(秒)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{"worker_type", "target"(全局总量), "quota", "mode"..., "persist"|...}
|
{"worker_type", "target"(全局总量), "mode", "sent_to", "reply"}
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: count 非法 / 类型无可用副本或本进程无其池
|
RuntimeError: count 非法 / 类型无可用副本
|
||||||
"""
|
"""
|
||||||
count = max(0, int(count))
|
count = max(0, int(count))
|
||||||
# 「count=全局总量」:写全局 cache 单一来源;本网关按在线网关分摊承担 quota。
|
# 「count=全局总量」收归 worker 层:网关不再直写 count/quota 缓存、不再直调进程池。
|
||||||
if self.cache is not None:
|
# 统一经 rescale 下发全局 target,由 worker 读取在线网关分摊、写缓存并扩缩本进程副本;
|
||||||
self.cache[self._scale_desired_key(worker_type)] = count
|
# inline/external 口径一致(inline 副本在本进程,经 Zenoh 直达其 __admin__)。
|
||||||
quota = self.own_quota(worker_type, count)
|
inline = False
|
||||||
logger.info(
|
|
||||||
"[scale] %s 全局目标=%d,本网关分摊 quota=%d", worker_type, count, quota
|
|
||||||
)
|
|
||||||
# inline:优先走本进程内池(按分摊配额),直接且即时应答
|
|
||||||
try:
|
try:
|
||||||
from chongming_worker.runtime import get_process_pool
|
from chongming_worker.runtime import get_process_pool
|
||||||
pool = get_process_pool(worker_type)
|
inline = get_process_pool(worker_type) is not None
|
||||||
except Exception: # noqa: BLE001 - 运行时缺依赖时退回 Zenoh 下发
|
except Exception: # noqa: BLE001 - 运行时缺依赖视为非 inline
|
||||||
pool = None
|
inline = False
|
||||||
if pool is not None:
|
candidates = [
|
||||||
await pool.set_target(quota)
|
w for w in self.list_workers()
|
||||||
persist = "未持久化"
|
if w["worker_type"] == worker_type and w.get("alive")
|
||||||
if self.cache is not None:
|
]
|
||||||
persist = f"count={count}(全局)已写 cache,本网关 quota={quota}"
|
if not candidates:
|
||||||
else:
|
raise RuntimeError(f"类型 '{worker_type}' 没有可用副本,无法调整数量")
|
||||||
persist = f"cache 未启用,quota={quota}(重启后将按配置还原)"
|
|
||||||
logger.info(
|
|
||||||
"[scale] %s inline 池已调整至 %d 副本(全局目标 %d)",
|
|
||||||
worker_type, quota, count,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"worker_type": worker_type,
|
|
||||||
"target": count,
|
|
||||||
"quota": quota,
|
|
||||||
"mode": "inline",
|
|
||||||
"reply": {"status": "accepted", "cmd": "rescale", "persist": persist},
|
|
||||||
}
|
|
||||||
|
|
||||||
# external(或本进程无池):下发 rescale 给任一存活副本,由其进程内池扩缩+持久化
|
# 目标选择:
|
||||||
|
# - inline:只下发本网关进程内的池(任意一个 inline 候选都会落到本地池,幂等);
|
||||||
|
# - external:同一类型可能有多个独立进程/实例,必须广播到全部存活实例,由各自按
|
||||||
|
# 「全局 count ÷ 存活实例数」重新分摊,避免只缩/扩其中一个进程造成不一致。
|
||||||
|
if inline:
|
||||||
|
inline_cands = [c for c in candidates if c.get("mode") == "inline"] or candidates
|
||||||
|
targets = [inline_cands[0]]
|
||||||
|
else:
|
||||||
|
targets = candidates
|
||||||
|
replies: Dict[str, Any] = {}
|
||||||
|
for cand in targets:
|
||||||
|
try:
|
||||||
|
replies[cand["worker_id"]] = await self._send_admin_cmd(
|
||||||
|
worker_type, cand["worker_id"], "rescale",
|
||||||
|
timeout=timeout, data_extra={"scale": count},
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001 - 单个实例失败不影响其它实例
|
||||||
|
logger.warning("[scale] %s 下发失败 %s: %s", worker_type, cand["worker_id"], e)
|
||||||
|
if not replies:
|
||||||
|
raise RuntimeError(f"类型 '{worker_type}' 没有副本响应 rescale,无法调整数量")
|
||||||
|
logger.info(
|
||||||
|
"[scale] %s 全局目标=%d 下发 %d 个实例(%s)",
|
||||||
|
worker_type, count, len(replies), "inline" if inline else "external",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"worker_type": worker_type,
|
||||||
|
"target": count,
|
||||||
|
"mode": "inline" if inline else "external",
|
||||||
|
"sent_to": list(replies.keys()),
|
||||||
|
"reply": replies,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def list_instances(
|
||||||
|
self,
|
||||||
|
worker_type: str,
|
||||||
|
timeout: float = 3.0,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""按「外部进程实例」分组盘点某类型的落地实体。
|
||||||
|
|
||||||
|
对类型下每个存活副本发轻量 status 查询,从回复取 instance_id 归组;
|
||||||
|
一个副本组 = 一个能独立承载若干副本的执行单元(inline=本网关进程/网关 host,
|
||||||
|
external=独立进程/容器)。副本数=组内在线副本个数,worker_ids 供精细缩放寻址。
|
||||||
|
"""
|
||||||
|
workers = [
|
||||||
|
w for w in self.list_workers()
|
||||||
|
if w["worker_type"] == worker_type and w.get("alive")
|
||||||
|
]
|
||||||
|
groups: Dict[str, Dict[str, Any]] = {}
|
||||||
|
for w in workers:
|
||||||
|
try:
|
||||||
|
reply = await self._send_admin_cmd(
|
||||||
|
worker_type, w["worker_id"], "query", timeout=timeout
|
||||||
|
)
|
||||||
|
iid = reply.get("instance_id") or "unknown"
|
||||||
|
except Exception as e: # noqa: BLE001 - 单副本查询失败不阻塞整体
|
||||||
|
logger.warning("[instances] %s/%s 查询失败: %s", worker_type, w["worker_id"], e)
|
||||||
|
iid = "unknown"
|
||||||
|
g = groups.setdefault(iid, {
|
||||||
|
"worker_type": worker_type,
|
||||||
|
"instance_id": iid,
|
||||||
|
"mode": w.get("mode", "external"),
|
||||||
|
"replicas": 0,
|
||||||
|
"worker_ids": [],
|
||||||
|
})
|
||||||
|
g["replicas"] += 1
|
||||||
|
g["worker_ids"].append(w["worker_id"])
|
||||||
|
groups.pop("unknown", None) # 拿不到身份的一律归入 unknown,避免误当独立实例
|
||||||
|
return list(groups.values())
|
||||||
|
|
||||||
|
async def scale_instance(
|
||||||
|
self,
|
||||||
|
worker_type: str,
|
||||||
|
instance_id: str,
|
||||||
|
count: int,
|
||||||
|
timeout: float = 5.0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""把某个外部进程/**实例**精确调到 count 个副本(绝对数,不影响其它实例)。
|
||||||
|
|
||||||
|
经该实例任一副本的 admin Queryable 下发 `rescale{scale:count, instance_id,
|
||||||
|
absolute:true}`,由其进程内 WorkerPool 直接 set_target(count) 并写入 instcounts
|
||||||
|
覆盖表(进程重启后仍生效);目标其它实例的指令被 worker 安全忽略。
|
||||||
|
返回与原 /scale 结构兼容:{worker_type, target, mode, sent_to, reply}。
|
||||||
|
"""
|
||||||
|
count = max(0, int(count))
|
||||||
|
insts = await self.list_instances(worker_type)
|
||||||
|
inst = next((i for i in insts if i["instance_id"] == instance_id), None)
|
||||||
|
if inst is None or not inst["worker_ids"]:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"类型 '{worker_type}' 下没有在线副本归属实例 '{instance_id}',无法调整数量"
|
||||||
|
)
|
||||||
|
wid = inst["worker_ids"][0]
|
||||||
|
reply = await self._send_admin_cmd(
|
||||||
|
worker_type, wid, "rescale",
|
||||||
|
timeout=timeout,
|
||||||
|
data_extra={"scale": count, "instance_id": instance_id, "absolute": True},
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"[scale_instance] %s 实例=%s 目标=%d → 下发 %s", worker_type, instance_id, count, wid,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"worker_type": worker_type,
|
||||||
|
"instance_id": instance_id,
|
||||||
|
"target": count,
|
||||||
|
"mode": inst.get("mode", "external"),
|
||||||
|
"sent_to": wid,
|
||||||
|
"reply": reply,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_worker_config(
|
||||||
|
self,
|
||||||
|
worker_type: str,
|
||||||
|
timeout: float = 5.0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""经 Zenoh 向该类型任一存活副本查询其 [workers.config]。
|
||||||
|
|
||||||
|
由 worker 访问自身缓存(或回退 toml 种子)返回;网关不直接接触外部 worker 的缓存后端。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{"status", "config": {...}, "source": "cache"|"toml", "worker_id", ...}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 无存活副本 / 副本未响应(可能运行旧版运行时无 get_config 能力)
|
||||||
|
"""
|
||||||
candidate = next(
|
candidate = next(
|
||||||
(
|
(
|
||||||
w for w in self.list_workers()
|
w for w in self.list_workers()
|
||||||
@@ -885,18 +1027,16 @@ class WorkerManager:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if candidate is None:
|
if candidate is None:
|
||||||
raise RuntimeError(f"类型 '{worker_type}' 没有可用副本,无法调整数量")
|
raise RuntimeError(f"类型 '{worker_type}' 没有在线副本,无法读取其配置")
|
||||||
|
|
||||||
reply = await self._send_admin_cmd(
|
reply = await self._send_admin_cmd(
|
||||||
worker_type, candidate["worker_id"], "rescale",
|
worker_type, candidate["worker_id"], "get_config", timeout=timeout
|
||||||
timeout=timeout, data_extra={"scale": count},
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"worker_type": worker_type,
|
"worker_type": worker_type,
|
||||||
"target": count,
|
"worker_id": candidate["worker_id"],
|
||||||
"mode": "external",
|
"status": reply.get("status"),
|
||||||
"sent_to": candidate["worker_id"],
|
"config": reply.get("config"),
|
||||||
"reply": reply,
|
"source": reply.get("source", "toml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def update_worker_config(
|
async def update_worker_config(
|
||||||
@@ -908,12 +1048,12 @@ class WorkerManager:
|
|||||||
"""经 Zenoh 下发 config_set 指令,让目标类型的全部副本修改配置并自我重启。
|
"""经 Zenoh 下发 config_set 指令,让目标类型的全部副本修改配置并自我重启。
|
||||||
|
|
||||||
worker 侧行为(见 chongming_worker.runtime):
|
worker 侧行为(见 chongming_worker.runtime):
|
||||||
- 仅 index0 副本把 updates 写入插件 config.toml 的 [workers.config](原子写回,
|
- 仅 index0 副本把 updates 写入共享缓存的 [workers.config](key
|
||||||
路由等保留键会被拒绝);
|
worker:cm:configs:{type},全局唯一,路由/保留键会被拒绝);不再回写 config.toml。
|
||||||
- 所有副本合并 updates 后以新配置重启(新 worker_id)。
|
- 所有副本合并 updates 后以新配置重启(新 worker_id)。
|
||||||
|
|
||||||
本方法只负责"下发 + 汇总",写操作由 worker 触发执行,故进程插件位于
|
本方法只负责"下发 + 汇总",写操作由 worker 访问其自身缓存触发执行,故进程插件
|
||||||
不同路径/宿主机时也可正确写回各自的配置文件。
|
位于不同路径/宿主机时也能把配置持久化到各自的共享缓存后端。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
worker_type: 插件类型
|
worker_type: 插件类型
|
||||||
|
|||||||
@@ -166,6 +166,72 @@ async def scale_workers(
|
|||||||
return {"success": True, "result": result}
|
return {"success": True, "result": result}
|
||||||
|
|
||||||
|
|
||||||
|
class ScaleInstanceBody(BaseModel):
|
||||||
|
"""逐实例精细扩缩请求体:目标实例 + 该实例的目标副本数。"""
|
||||||
|
instance_id: str = Field(..., description="外部进程/实例身份(list_instances 返回)")
|
||||||
|
count: int = Field(..., ge=0, description="该实例的目标副本数(绝对数,>=0)")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/admin/workers/{worker_type}/instances",
|
||||||
|
summary="按实例分组列出某类型 worker(精细扩缩寻址)",
|
||||||
|
)
|
||||||
|
async def list_worker_instances(
|
||||||
|
request: Request,
|
||||||
|
worker_type: str,
|
||||||
|
_admin: dict = Depends(require_admin),
|
||||||
|
):
|
||||||
|
"""把某类型的在线副本按「外部进程实例」分组返回。
|
||||||
|
|
||||||
|
每个实例 = 一个可独立承载副本的执行单元(inline=本网关进程/host,
|
||||||
|
external=独立进程/容器),含当前副本数与可选中的 worker_id。
|
||||||
|
"""
|
||||||
|
manager = _get_manager(request)
|
||||||
|
try:
|
||||||
|
instances = await manager.list_instances(worker_type)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("列出实例失败: %s 错误=%s", worker_type, e)
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
return {"success": True, "result": instances}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/admin/workers/{worker_type}/scale_instance",
|
||||||
|
summary="把某实例精确扩缩到指定副本数(精细扩缩容)",
|
||||||
|
)
|
||||||
|
async def scale_worker_instance(
|
||||||
|
request: Request,
|
||||||
|
worker_type: str,
|
||||||
|
body: ScaleInstanceBody,
|
||||||
|
_admin: dict = Depends(require_admin),
|
||||||
|
):
|
||||||
|
"""把指定外部进程/**实例**精确调整到 body.count 个副本(绝对数),不影响其它实例。
|
||||||
|
|
||||||
|
经该实例任一副本下发 rescale{scale, instance_id, absolute:true},由其进程内
|
||||||
|
WorkerPool 直接扩缩并记入逐实例覆盖表;重启后仍保持该绝对数。
|
||||||
|
"""
|
||||||
|
manager = _get_manager(request)
|
||||||
|
try:
|
||||||
|
result = await manager.scale_instance(
|
||||||
|
worker_type, body.instance_id, body.count
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(
|
||||||
|
"逐实例扩缩失败: %s instance=%s -> %d 用户=%s 错误=%s",
|
||||||
|
worker_type, body.instance_id, body.count,
|
||||||
|
_admin.get("username", _admin.get("sub", "-")),
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
logger.info(
|
||||||
|
"逐实例扩缩: %s instance=%s -> %d 用户=%s 结果=%s",
|
||||||
|
worker_type, body.instance_id, body.count,
|
||||||
|
_admin.get("username", _admin.get("sub", "-")),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return {"success": True, "result": result}
|
||||||
|
|
||||||
|
|
||||||
class ConfigUpdateBody(BaseModel):
|
class ConfigUpdateBody(BaseModel):
|
||||||
"""配置更新请求体:写目标插件 config.toml 的 [workers.config] 段。"""
|
"""配置更新请求体:写目标插件 config.toml 的 [workers.config] 段。"""
|
||||||
updates: dict = Field(..., description="待写入 [workers.config] 的键值(路由/加载控制键除外)")
|
updates: dict = Field(..., description="待写入 [workers.config] 的键值(路由/加载控制键除外)")
|
||||||
@@ -179,11 +245,29 @@ async def read_worker_config(
|
|||||||
):
|
):
|
||||||
"""读取某插件 config.toml 的 [workers.config] 现有值(供前端展示)。
|
"""读取某插件 config.toml 的 [workers.config] 现有值(供前端展示)。
|
||||||
|
|
||||||
直接从插件配置文件读取,反映最近一次落盘结果(含 worker 写回后的值)。
|
经 Zenoh 向该类任一存活副本下发 get_config,由其访问自身缓存(或回退 toml 种子)
|
||||||
|
返回 —— 网关不直接接触 external worker 的缓存后端。无存活副本 / 旧版运行时
|
||||||
|
不支持 get_config 时,回退回直读 config/workers/<type>/config.toml 的 [workers].config。
|
||||||
路由配置(add.toml 等)不在此范围。
|
路由配置(add.toml 等)不在此范围。
|
||||||
|
|
||||||
- worker_type: 插件类型(如 example)
|
- worker_type: 插件类型(如 example)
|
||||||
"""
|
"""
|
||||||
|
manager = _get_manager(request)
|
||||||
|
try:
|
||||||
|
info = await manager.get_worker_config(worker_type)
|
||||||
|
cfg = info.get("config")
|
||||||
|
if isinstance(cfg, dict) and info.get("status") in ("ok", None):
|
||||||
|
return {
|
||||||
|
"worker_type": worker_type,
|
||||||
|
"worker_id": info.get("worker_id"),
|
||||||
|
"source": info.get("source", "cache"),
|
||||||
|
"config": cfg,
|
||||||
|
"success": True,
|
||||||
|
}
|
||||||
|
logger.debug("worker get_config 未返回有效配置(%s),回退读 toml", info)
|
||||||
|
except Exception as e: # noqa: BLE001 - 副本未响应等一律回退读 toml
|
||||||
|
logger.debug("worker get_config 查询失败,回退读 toml: %s", e)
|
||||||
|
|
||||||
path = pathlib.Path("config/workers") / worker_type / "config.toml"
|
path = pathlib.Path("config/workers") / worker_type / "config.toml"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise HTTPException(status_code=404, detail=f"插件 {worker_type} 的配置文件不存在: {path}")
|
raise HTTPException(status_code=404, detail=f"插件 {worker_type} 的配置文件不存在: {path}")
|
||||||
@@ -194,6 +278,7 @@ async def read_worker_config(
|
|||||||
return {
|
return {
|
||||||
"worker_type": worker_type,
|
"worker_type": worker_type,
|
||||||
"config": cfg if isinstance(cfg, dict) else {},
|
"config": cfg if isinstance(cfg, dict) else {},
|
||||||
|
"source": "toml",
|
||||||
"success": True,
|
"success": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
原生 FastAPI 对照服务(无 Zenoh / 无网关中间件 / 无 worker)
|
||||||
|
|
||||||
|
用于与 chongming 网关的 /api/v1/demo/hello 做性能对照:
|
||||||
|
同样的 GET 返回一个 JSON(字段形状与 demo/hello 一致),排除业务与 RPC 链路,
|
||||||
|
仅度量纯 FastAPI+Uvicorn 框架本身的吞吐与延迟。
|
||||||
|
|
||||||
|
启动:
|
||||||
|
uv run uvicorn native_fastapi:app --host 0.0.0.0 --port 8001 --no-access-log
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/hello")
|
||||||
|
async def hello(name: str = "bench"):
|
||||||
|
return {"greeting": f"Hello, {name}!", "worker_id": "native", "timestamp": time.time()}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""
|
||||||
|
外部进程多实例全局分摊 —— distribute_quota 逻辑单测(无需网关/缓存后端)。
|
||||||
|
|
||||||
|
用 dict 作缓存桩,模拟同类型多个外部进程实例注册,验证扩缩容时全局 count 按
|
||||||
|
「存活实例数 N」分摊(而非每个进程都全额),以及实例退出/租约过期后的接管行为。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../utils/worker/src"))
|
||||||
|
|
||||||
|
import chongming_worker.runtime as r
|
||||||
|
|
||||||
|
PASS = 0
|
||||||
|
FAIL = 0
|
||||||
|
|
||||||
|
|
||||||
|
def report(name, cond, detail=""):
|
||||||
|
global PASS, FAIL
|
||||||
|
if cond:
|
||||||
|
PASS += 1
|
||||||
|
print(f"✅ {name}" + (f" — {detail}" if detail else ""))
|
||||||
|
else:
|
||||||
|
FAIL += 1
|
||||||
|
print(f"❌ {name}" + (f" — {detail}" if detail else ""))
|
||||||
|
|
||||||
|
|
||||||
|
def fake_cache():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def as_instance(iid, cache, worker_type="demo"):
|
||||||
|
"""把当前身份设为 iid,并在缓存登记该实例参与方。"""
|
||||||
|
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
|
||||||
|
try:
|
||||||
|
r.register_instance(worker_type, cache)
|
||||||
|
finally:
|
||||||
|
if old is None:
|
||||||
|
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
else:
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = old
|
||||||
|
|
||||||
|
|
||||||
|
def quota_as(iid, worker_type, total, cache):
|
||||||
|
"""以指定实例身份计算其分摊配额(external 分支)。"""
|
||||||
|
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
|
||||||
|
try:
|
||||||
|
return r.distribute_quota(cache, worker_type, total, gw_id=None)
|
||||||
|
finally:
|
||||||
|
if old is None:
|
||||||
|
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
else:
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = old
|
||||||
|
|
||||||
|
|
||||||
|
def unreg_instance(iid, cache, worker_type="demo"):
|
||||||
|
"""以指定实例身份从参与方列表移除它。"""
|
||||||
|
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = iid
|
||||||
|
try:
|
||||||
|
r.unregister_instance(worker_type, cache)
|
||||||
|
finally:
|
||||||
|
if old is None:
|
||||||
|
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
else:
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = old
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
cache = fake_cache()
|
||||||
|
wt = "demo"
|
||||||
|
|
||||||
|
# 1. 单实例(未启用实例注册,参与方为空)→ 兜底全额(兼容旧行为)
|
||||||
|
q = r.distribute_quota(cache, wt, 6, gw_id=None)
|
||||||
|
report("单实例无参与方 → 全额承担 6", q == 6, f"got={q}")
|
||||||
|
|
||||||
|
# 2. 两个实例并存:全局 6 分摊 → 各 3
|
||||||
|
as_instance("inst-a", cache, wt)
|
||||||
|
as_instance("inst-b", cache, wt)
|
||||||
|
qa = quota_as("inst-a", wt, 6, cache)
|
||||||
|
qb = quota_as("inst-b", wt, 6, cache)
|
||||||
|
report("两个实例并存 → 各分 3", qa == 3 and qb == 3, f"a={qa}, b={qb}")
|
||||||
|
|
||||||
|
# 3. 三个实例、总数不能整除:7 // 3 余 1 → 按序给第一个补 1
|
||||||
|
as_instance("inst-c", cache, wt)
|
||||||
|
qs = {i: quota_as(i, wt, 7, cache) for i in ("inst-a", "inst-b", "inst-c")}
|
||||||
|
report(
|
||||||
|
"三个实例 ≠整除 → 总和=7 且余量补靠前实例",
|
||||||
|
sum(qs.values()) == 7 and set(qs.values()) == {2, 2, 3},
|
||||||
|
f"{qs}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 一个实例退出(unregister)→ 其余接管全部
|
||||||
|
unreg_instance("inst-c", cache, wt)
|
||||||
|
qa2 = quota_as("inst-a", wt, 6, cache)
|
||||||
|
qb2 = quota_as("inst-b", wt, 6, cache)
|
||||||
|
report(
|
||||||
|
"实例退出后由剩余接管 → 各分 3",
|
||||||
|
qa2 == 3 and qb2 == 3,
|
||||||
|
f"a={qa2}, b={qb2}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 租约过期清理:伪造一个过期的参与方,读实例列表应排除它
|
||||||
|
cache[r._SCALE_INSTANCES_KEY.format(type=wt)] = [
|
||||||
|
["inst-stale", time.time() - r._INSTANCE_LEASE - 10],
|
||||||
|
["inst-fresh", time.time()],
|
||||||
|
]
|
||||||
|
ids = r._read_instances(cache, wt)
|
||||||
|
report(
|
||||||
|
"死实例按租约剔除",
|
||||||
|
ids == ["inst-fresh"],
|
||||||
|
f"instances={ids}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. 实例数=1 时全额(不误分摊)
|
||||||
|
cache[r._SCALE_INSTANCES_KEY.format(type=wt)] = [
|
||||||
|
[r.current_instance_id(), time.time()]
|
||||||
|
]
|
||||||
|
q1 = r.distribute_quota(cache, wt, 5, gw_id=None)
|
||||||
|
report("单存活实例 → 全额 5", q1 == 5, f"got={q1}")
|
||||||
|
|
||||||
|
# 7. inline(有网关身份)路径回归:按在线网关分摊不受影响
|
||||||
|
cache[r._SCALE_GATEWAYS_KEY] = [["gw-a", time.time()], ["gw-b", time.time()]]
|
||||||
|
qa_in = r.distribute_quota(cache, wt, 6, "gw-a")
|
||||||
|
qb_in = r.distribute_quota(cache, wt, 6, "gw-b")
|
||||||
|
report(
|
||||||
|
"inline 按网关分摊回归 → 各 3",
|
||||||
|
qa_in == 3 and qb_in == 3,
|
||||||
|
f"gw-a={qa_in}, gw-b={qb_in}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 8. 逐实例绝对覆盖:写入后能被读回且供启动解析(external)使用
|
||||||
|
r._write_own_instance_count(cache, wt, 4, "inst-a")
|
||||||
|
r._write_own_instance_count(cache, wt, 2, "inst-b")
|
||||||
|
report(
|
||||||
|
"instcounts 覆盖写入/读回",
|
||||||
|
r._own_instance_count(cache, wt, "inst-a") == 4
|
||||||
|
and r._own_instance_count(cache, wt, "inst-b") == 2,
|
||||||
|
f"{r._read_instance_counts(cache, wt)}",
|
||||||
|
)
|
||||||
|
# 启动解析:external(gw_id=None)有绝对覆盖时直接采用,而非被全局均分
|
||||||
|
old = os.environ.get("CHONGMING_WORKER_INSTANCE")
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = "inst-a"
|
||||||
|
try:
|
||||||
|
restored = r.resolve_worker_count(wt, 6, cache, "cache", gw_id=None)
|
||||||
|
finally:
|
||||||
|
if old is None:
|
||||||
|
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
else:
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = old
|
||||||
|
report("resolve_worker_count 启动时采用本实例绝对数", restored == 4, f"got={restored}")
|
||||||
|
|
||||||
|
# 9. 清除覆盖后回退全局均分
|
||||||
|
r._clear_instance_counts(cache, wt)
|
||||||
|
report(
|
||||||
|
"清除覆盖后覆盖表为空",
|
||||||
|
r._read_instance_counts(cache, wt) == {},
|
||||||
|
f"{r._read_instance_counts(cache, wt)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 10. 缺省实例身份唯一(hostname:pid):同机未指定 --instance 的多进程不会并成一个参与方
|
||||||
|
r.set_current_instance_id(None)
|
||||||
|
old_env = os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
try:
|
||||||
|
default_iid = r.current_instance_id()
|
||||||
|
finally:
|
||||||
|
if old_env is not None:
|
||||||
|
os.environ["CHONGMING_WORKER_INSTANCE"] = old_env
|
||||||
|
else:
|
||||||
|
os.environ.pop("CHONGMING_WORKER_INSTANCE", None)
|
||||||
|
import socket as _socket
|
||||||
|
report(
|
||||||
|
"缺省实例身份含进程区分({host}:{pid})",
|
||||||
|
default_iid == f"{_socket.gethostname()}:{os.getpid()}",
|
||||||
|
f"got={default_iid}",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "=" * 56)
|
||||||
|
print(f"测试结果: ✅ {PASS} 通过 ❌ {FAIL} 失败")
|
||||||
|
return 0 if FAIL == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(run())
|
||||||
@@ -541,18 +541,18 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chongming-worker"
|
name = "chongming-worker"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "chongming-cache" },
|
||||||
{ name = "eclipse-zenoh" },
|
{ name = "eclipse-zenoh" },
|
||||||
{ name = "minio" },
|
{ name = "minio" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
{ name = "tomli-w" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-worker/0.4.0/chongming_worker-0.4.0.tar.gz", hash = "sha256:c2b13fc1c2eb262f0ca5a1b64035bff1881a912c5a95446a9ce77b1ba968b03b" }
|
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-worker/0.4.1/chongming_worker-0.4.1.tar.gz", hash = "sha256:ea8c1017e2c3c517475bc66ebdfecfc662b318fdb19a02e9003987637299ee3c" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-worker/0.4.0/chongming_worker-0.4.0-py3-none-any.whl", hash = "sha256:96e1dd6b8c22a294b83b54d8ac3723f4555865625ed210a6e1974bc6ac3c4a9d" },
|
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-worker/0.4.1/chongming_worker-0.4.1-py3-none-any.whl", hash = "sha256:fd8202890fb124a0180e04592566882b335c3ddc456b7509a50932ddcc008279" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1098,9 +1098,9 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuitka"
|
name = "nuitka"
|
||||||
version = "4.1.3"
|
version = "4.2"
|
||||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/d8/bdb7febea4b4fe5d3d6fe2610f946771f03e792e05a5e8ec00b62c00b265/nuitka-4.1.3.tar.gz", hash = "sha256:838ff8899dc2f0b652d4fcf6c5d7466cb7ad5abcb005668ac622d1e40f4d8a8d", size = 4565475, upload-time = "2026-06-21T10:48:04.369Z" }
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/f7/305d1eca71f554f52cac820b8d1765aeb5fb2b3732d7f49f5a32aa4d97d2/nuitka-4.2.tar.gz", hash = "sha256:b6112c10a5d0431e2b4309780257a03049a42605290e83f771d57a067a387410", size = 4628471, upload-time = "2026-08-27T14:17:30.409Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
@@ -1608,27 +1608,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.16.4"
|
version = "0.16.5"
|
||||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" }
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" },
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1783,15 +1783,6 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tomli-w"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
|
||||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "transitions"
|
name = "transitions"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@@ -1905,7 +1896,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "volcengine-python-sdk"
|
name = "volcengine-python-sdk"
|
||||||
version = "5.0.46"
|
version = "5.0.47"
|
||||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -1913,9 +1904,9 @@ dependencies = [
|
|||||||
{ name = "six" },
|
{ name = "six" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/c6/2094e5ef1303393aa78599cd750f95f32143643ee330ddfe0b23faaf59fd/volcengine_python_sdk-5.0.46.tar.gz", hash = "sha256:3d7ee12ad559d07bb53a160b4ba98feaeee09f4a03d87a86855b162571ff717a", size = 10694620, upload-time = "2026-08-20T11:55:35.039Z" }
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/2a/25dcb01d4f2a4629bfd46dee0972d57c590e29e9d7271a918985593ba414/volcengine_python_sdk-5.0.47.tar.gz", hash = "sha256:64dc6f4c591e27972f5331f097f0d5926adf96f3643c264df0cc0007f6bf0d8d", size = 10748617, upload-time = "2026-08-27T12:07:11.774Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/f0/171e760424849eabfe735c4b37e29949b2279555dd1654819db973ee7be7/volcengine_python_sdk-5.0.46-py2.py3-none-any.whl", hash = "sha256:f73e8f675f9ff545d0fec54b7057436dba0f1c420fcb235848a76a5120e5bf18", size = 43011218, upload-time = "2026-08-20T11:55:28.333Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cf/d6/8ddd4f13716af4fb86057b48d0166e915ab013a1e287d29b6b11cdbde891/volcengine_python_sdk-5.0.47-py2.py3-none-any.whl", hash = "sha256:f2605e9a2a22143557fb9a58c9f1523032448cd6282add6387ac57df0510519b", size = 43168897, upload-time = "2026-08-27T12:07:04.749Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user