chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:34:57 +08:00
commit 1ccb4e58d2
80 changed files with 21133 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
# 多 Worker 模式指南
myboot 0.2.0 起,多 worker`workers > 1`)模式下**每个 worker 进程独立完成组件实例化**:
client / service / controller 都在各自的 worker 进程内创建,不再共享父进程 fork 前的实例。
本文是多 worker 相关能力的一站式说明。
## 启动方式
```python
# main.py
app = create_app(name="my-app", auto_discover_package="app")
if __name__ == "__main__":
app.run(workers=4, app_path="main:app.get_fastapi_app()")
```
```yaml
# conf/config.yaml 亦可
server:
workers: 4
```
无需再手动调用 `auto_discover()` / `apply_auto_configuration()`——框架在每个
worker 内自动完成引导(`bootstrap_worker()`)。
## Worker 信息
```python
app.worker_id # 当前 worker ID,从 1 开始;单进程为 1
app.worker_count # worker 总数
app.is_primary_worker # 是否主 worker(适合"只跑一份"的逻辑)
```
## Client 生命周期
**建连可以直接写在 `__init__` 里**——0.2.0 起 client 在各 worker 内实例化,
不存在 fork 共享连接问题:
```python
@client()
class RedisClient:
def __init__(self):
self.conn = redis.Redis(...) # 安全:本 worker 进程内新建
def close(self): # 可选:定义即获得自动清理
self.conn.close()
```
定义了 `close()` 方法(同步或 async)的 client,框架会在 worker 停止时
`worker_stop_hooks` 之后、`shutdown_hooks` 之前)自动调用。若你也在自己的
shutdown 钩子里关闭,建议把 `close()` 实现为幂等——框架的兜底调用会把二次
close 的异常降为 warning,不影响关闭流程。
## Worker 生命周期钩子
```python
from myboot.core.decorators import on_worker_start, on_worker_stop
@on_worker_start
def warm_up():
"""每个 worker 启动时各触发一次(startup_hooks 之后、调度器启动之前)"""
@on_worker_stop(order=1)
def cleanup():
"""每个 worker 停止时各触发一次(调度器停止之后)"""
```
> Windows 限制:多 worker 下父进程以 `terminate()` 硬终止 worker
> `@on_worker_stop` 与 client 自动 close 在 Windows 上不保证执行;
> 关键清理逻辑不要只依赖它们。
## Primary-first 初始化协调:`run_primary_first`
典型场景:4 个 worker 都去下载同一个 3GB 模型是浪费——应该 primary 下载,
其余 worker 等它完成后直接从本地加载:
```python
from myboot.utils import run_primary_first
from myboot.core.decorators import on_worker_start
@on_worker_start
def load_model():
run_primary_first(
"sasrec-model", # 协调标识(同一标识共享一次协调)
primary_fn=download_and_load, # 仅 primary 执行
secondary_fn=load_from_local, # 其余 worker 等 primary 完成后执行
timeout=600, # 等待上限(秒)
)
```
语义保证:
- `secondary_fn` 缺省时其余 worker 也执行 `primary_fn`(即"等完再做同样的事");
- primary 失败 → 其余 worker 立即抛 `RuntimeError`(含原始异常信息),不会傻等到超时;
- 等待超时 → `TimeoutError`
- 单进程模式退化为直接调用 `primary_fn()`,代码无需分支;
- 基于临时目录标记文件实现,跨 spawn/fork、跨平台(不依赖 fcntl),
但**不支持跨机器**协调。
## 定时任务与多 worker:任务级 `all_workers`
默认所有定时任务**只在 primary worker 执行一份**(防止重复)。但"刷新本 worker
内存态"的任务必须每个 worker 各跑一份,否则其余 worker 永远用旧数据:
```python
@component()
class RefreshJobs:
@interval(hours=24, all_workers=True) # 每个 worker 各执行
def refresh_local_cache(self):
self.items = load_items() # 刷新的是本进程内存
@cron("0 2 * * *") # 默认:仅 primary 执行一份
def daily_report(self):
send_report()
```
怎么选:
| 任务类型 | 配置 | 原因 |
|---|---|---|
| 刷新进程内缓存/内存清单 | `all_workers=True` | 数据在各 worker 自己内存里 |
| 发报表 / 写数据库 / 调外部 API | 默认 | 跑多份会重复副作用 |
全局开关 `scheduler.on_all_workers: true`(所有任务都在全部 worker 跑)仍然有效,
但通常任务级参数是更对的粒度。`scheduler.enabled: false` 全局禁用一切定时任务。
## 服务作用域 `scope`
```python
@service() # 默认 singleton:每个 worker 进程内一个实例
@service(scope="request") # 每个请求(asyncio 任务上下文)一个实例
@service(scope="factory") # 每次注入解析都创建新实例
```
`request` 作用域适合存放请求级上下文,避免单例服务里的共享可变状态。
`@client` 同样支持 `scope` 参数。