xschedy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xschedy might be problematic. Click here for more details.
- xschedy-0.1.0/.gitignore +21 -0
- xschedy-0.1.0/LICENSE +21 -0
- xschedy-0.1.0/PKG-INFO +360 -0
- xschedy-0.1.0/README.md +328 -0
- xschedy-0.1.0/pyproject.toml +114 -0
- xschedy-0.1.0/xschedy/__init__.py +6 -0
- xschedy-0.1.0/xschedy/cli.py +197 -0
- xschedy-0.1.0/xschedy/core/__init__.py +1 -0
- xschedy-0.1.0/xschedy/core/app.py +182 -0
- xschedy-0.1.0/xschedy/core/constants.py +356 -0
- xschedy-0.1.0/xschedy/core/heartbeat.py +1354 -0
- xschedy-0.1.0/xschedy/core/logging.py +164 -0
- xschedy-0.1.0/xschedy/core/redis_client.py +849 -0
- xschedy-0.1.0/xschedy/core/s3_client.py +392 -0
- xschedy-0.1.0/xschedy/core/states.py +230 -0
- xschedy-0.1.0/xschedy/core/threads.py +80 -0
- xschedy-0.1.0/xschedy/core/timeutil.py +82 -0
- xschedy-0.1.0/xschedy/worker/__init__.py +83 -0
xschedy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Runtime artifacts
|
|
13
|
+
*.db
|
|
14
|
+
logs/
|
|
15
|
+
|
|
16
|
+
# 审查过程产物(非交付物):对照项目的取证记录(matrix/*.md)与一次性工具脚本。
|
|
17
|
+
# 内容有参考价值(design 2.3 对比矩阵的原文出处),但不属于要发布/部署的东西。
|
|
18
|
+
.superpowers/
|
|
19
|
+
|
|
20
|
+
# 打包快照
|
|
21
|
+
*.rar
|
xschedy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zxpshadow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xschedy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xschedy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 内部系统的异步任务调度器
|
|
5
|
+
Author: zxpshadow
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: celery,distributed,heartbeat,scheduler,task-queue,timeout
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: boto3>=1.34
|
|
18
|
+
Requires-Dist: celery==5.6.3
|
|
19
|
+
Requires-Dist: loguru>=0.7
|
|
20
|
+
Requires-Dist: redis>=5.0
|
|
21
|
+
Provides-Extra: server
|
|
22
|
+
Requires-Dist: croniter>=2.0; extra == 'server'
|
|
23
|
+
Requires-Dist: cryptography>=42.0; extra == 'server'
|
|
24
|
+
Requires-Dist: fastapi>=0.111; extra == 'server'
|
|
25
|
+
Requires-Dist: httpx>=0.27; extra == 'server'
|
|
26
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'server'
|
|
27
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'server'
|
|
28
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'server'
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'server'
|
|
30
|
+
Provides-Extra: worker
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Xschedy
|
|
34
|
+
|
|
35
|
+
内部系统的异步任务调度器,基于 Celery 之上的封装层(复用其 events / result / control 能力)。
|
|
36
|
+
|
|
37
|
+
技术栈:Celery + RabbitMQ(broker)+ Redis(backend)+ SQLAlchemy(历史落库)+ FastAPI(server)+ Prometheus(指标)。
|
|
38
|
+
|
|
39
|
+
> 内部设计与审查记录随仓库走,**不在 PyPI 包里**;PyPI 上那个 `xschedy` 只有 worker 侧。
|
|
40
|
+
|
|
41
|
+
## 安装
|
|
42
|
+
|
|
43
|
+
**两个来源**(PyPI 上只有 worker 侧):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 业务侧:写业务函数 + 跑 worker。只装核心(celery / redis / loguru / boto3)。
|
|
47
|
+
pip install xschedy
|
|
48
|
+
|
|
49
|
+
# 控制面:从**仓库**装(PyPI 上没有控制面代码)。容器化见 deploy/。
|
|
50
|
+
pip install -e ".[server]" # 核心 + FastAPI / SQLAlchemy / Prometheus / psycopg …
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> ⚠️ **`xschedy[server]` 在 PyPI 上是空承诺**:PyPI 的包不含控制面代码,
|
|
54
|
+
> `pip install xschedy[server]` 只会装上那些依赖。**控制面请用上面第二条**(或 `deploy/Dockerfile` 的镜像)。
|
|
55
|
+
>
|
|
56
|
+
> ⚠️ 从 PyPI 装的包里**只有 worker 侧**:`xschedy worker …` 可用;
|
|
57
|
+
> `xschedy server …` / `db init` / `tasks` / `queues` / `periodic-tasks` 会**明确提示需要控制面**并退 2,
|
|
58
|
+
> 不是让人看裸 `ModuleNotFoundError`。本文档中**涉及控制面的命令**(下方「运行」里的 server 角色)要求从仓库装。
|
|
59
|
+
|
|
60
|
+
## 运行
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 前置:RabbitMQ + Redis(部署前提:rabbitmq.conf 调大 consumer_timeout)
|
|
64
|
+
|
|
65
|
+
# --- 控制面(需从仓库装)---
|
|
66
|
+
xschedy db init # 初始化数据库表
|
|
67
|
+
|
|
68
|
+
# 分别启动(生产)
|
|
69
|
+
xschedy server api
|
|
70
|
+
xschedy server events
|
|
71
|
+
xschedy server timeout
|
|
72
|
+
xschedy server callback
|
|
73
|
+
xschedy server beat
|
|
74
|
+
|
|
75
|
+
# --- worker(PyPI 装的包里就有)---
|
|
76
|
+
xschedy worker -Q my_queue --tasks myproject.tasks # --tasks:启动时加载的任务模块(逗号分隔),缺省不加载任何用户任务
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
server 5 个角色全部零 fork,Windows 宿主可原生跑。⚠️ **worker 侧在 Windows 必须显式 `--pool solo`**:prefork 的 spawn 子进程会崩(billiard `WinError 6/5`),**「billiard 在 Windows 会自动 spawn」这条说法是错的**(`cli.py` 的注释里写着它「未经验证、具误导性」)。Docker/Linux 用默认 prefork 即可。
|
|
81
|
+
## 写业务任务
|
|
82
|
+
|
|
83
|
+
任务定义在任意模块(如 `myproject/tasks.py`),启动 worker 时用 `--tasks` 指定加载(逗号分隔)。模块顶层 `@xschedy_heartbeat_task` 定义的任务才会注册进 worker;缺省 `--tasks` 则不加载任何用户任务,收到消息会报 `unregistered task` 丢弃。
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from xschedy.worker import xschedy_heartbeat_task, get_current_task
|
|
87
|
+
|
|
88
|
+
# 可选:worker 生命周期钩子(零 celery import)
|
|
89
|
+
from xschedy.worker import (
|
|
90
|
+
xschedy_worker_process_init, # 每个子进程初始化(建 DB 连接池等连接类资源)
|
|
91
|
+
xschedy_worker_process_shutdown, # 子进程退出前清理(收 pid, exitcode)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@xschedy_worker_process_init
|
|
95
|
+
def init_process():
|
|
96
|
+
global engine
|
|
97
|
+
engine = create_engine(DB_URL) # 子进程各建各的,不能放主进程钩子(prefork 继承 fd)
|
|
98
|
+
|
|
99
|
+
@xschedy_heartbeat_task(
|
|
100
|
+
name="order_pay",
|
|
101
|
+
heartbeat_interval=2.0, # 心跳间隔
|
|
102
|
+
timeout=600, # 硬超时
|
|
103
|
+
max_retries=3, # 重试次数
|
|
104
|
+
retry_backoff=True, # 指数退避
|
|
105
|
+
)
|
|
106
|
+
def order_pay(self, user_data): # 单参契约:user_data 是 dict(透传自请求体的 user_data 字段)
|
|
107
|
+
self.report_progress(current=1, total=3, description="第1步")
|
|
108
|
+
return {"paid": True}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
业务函数能拿到的参数分四类:
|
|
112
|
+
|
|
113
|
+
**① 签名参数**:`(self, user_data)` —— 请求体的 `user_data`(JSON 字符串)经框架 `json.loads` 后**透传,不合并**,业务函数收到的就是调用方传的那个 dict。
|
|
114
|
+
|
|
115
|
+
**② `self` 上的方法**(HeartbeatTask 基类):
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
self.report_progress(current, total, description) # 上报进度
|
|
119
|
+
self.increment_progress(by, description) # 递增进度
|
|
120
|
+
self.is_aborted() # 协作式中止检查(需 abortable=True,见下)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> ⚠️ `is_aborted()` **仅在装饰器传了 `abortable=True` 时才有效**——该参数决定心跳线程是否去读中止标记
|
|
124
|
+
> (`xschedy:abort:{task_id}`,由 `POST /api/tasks/{id}/abort` 写入)。未开启时它**恒返回 `False`**
|
|
125
|
+
> (进程内会打一次告警提醒),abort 端点写入的标记也无人消费——该端点是**尽力而为**语义,
|
|
126
|
+
> 返回 `abort_requested` 不代表目标任务会响应。按需开启:
|
|
127
|
+
>
|
|
128
|
+
> ```python
|
|
129
|
+
> @xschedy_heartbeat_task(name="order_pay", abortable=True)
|
|
130
|
+
> ```
|
|
131
|
+
|
|
132
|
+
**③ 框架字段(`get_current_task()`,contextvar 零污染读取,不混入 user_data)**:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
ctx = get_current_task()
|
|
136
|
+
ctx.task_id # 任务 id
|
|
137
|
+
ctx.task_name # 任务名
|
|
138
|
+
ctx.attempt # 第几次尝试
|
|
139
|
+
ctx.scheduled_time # 定时任务计划触发时刻(手动下发为 None)
|
|
140
|
+
ctx.default_args # 全局默认参数 XSCHEDY_DEFAULT_ARGS(透传不合并,按需读)
|
|
141
|
+
ctx.periodic_kwargs # 定时任务 PeriodicTask.kwargs(透传不合并,按需读)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**④ Celery 原生(`self.request` / `self`)**:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
self.request.id # task_id
|
|
148
|
+
self.request.retries # 重试次数(0 起)
|
|
149
|
+
self.request.hostname # 执行 worker 主机名
|
|
150
|
+
self.request.delivery_info # 投递信息(exchange/routing_key/priority)
|
|
151
|
+
self.request.headers # 完整 message headers
|
|
152
|
+
self.retry(exc=..., countdown=...) # 手动重试
|
|
153
|
+
self.name # 任务名
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### ⚠️ 三条必须知道的约束
|
|
157
|
+
|
|
158
|
+
**① `user_data` 里的凭证不加密,会明文进 DB 与日志。**
|
|
159
|
+
|
|
160
|
+
xschedy **不代管**调用方访问外部系统的凭证(不做 airflow 的 Connection/Variable 那套),
|
|
161
|
+
凭证由你自己管理、随 `user_data` 传进来。但「不代管」不等于「替你加密」——
|
|
162
|
+
|
|
163
|
+
- `user_data` 原样落进 `task_record.args`(TEXT 列,**明文**);
|
|
164
|
+
- 带 `task_id` 的任务日志同样会把它打出来(见「日志关联」)。
|
|
165
|
+
|
|
166
|
+
所以**不要把密码 / token 放进 `user_data`**。确实需要传的,传引用(如密钥管理系统里的 key 名),
|
|
167
|
+
由业务自己在 worker 侧去取。
|
|
168
|
+
|
|
169
|
+
> 与它对照:**xschedy 自己接收并存储的凭证是加密的**——`notify` 的 webhook URL / MQ 密码
|
|
170
|
+
> 走 `XSCHEDY_SECRET_KEY` 的 Fernet 加密。通知出口除此之外没有别的凭证:
|
|
171
|
+
> xschedy 服务端自己往外发的只有通知,没有第二条通道(已合并)。
|
|
172
|
+
|
|
173
|
+
**② 等外部事件的长任务,不要在 worker 里 `sleep` 轮询。**
|
|
174
|
+
|
|
175
|
+
`sleep` 会白占一个执行槽数小时。正确做法是用 Celery 原生的 `self.retry(countdown=N)` 把任务
|
|
176
|
+
重新入队,下次被调度时再检查一遍条件:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
@xschedy_heartbeat_task(name="wait_upstream", max_retries=100)
|
|
180
|
+
def wait_upstream(self, user_data):
|
|
181
|
+
if not upstream_ready(user_data["job_id"]):
|
|
182
|
+
self.retry(countdown=60) # 释放执行槽,60s 后再来看看
|
|
183
|
+
return {"done": True}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
效果等同轮询,成本几乎为零。(「不做 deferrable / triggerer」的替代实践。)
|
|
187
|
+
|
|
188
|
+
**③ CPU 密集型任务要在关键点让出 GIL,否则心跳会「假死」。**
|
|
189
|
+
|
|
190
|
+
心跳跑在一个**独立的 Python 线程**里(`core/heartbeat.py`)。CPython 的 GIL 下,
|
|
191
|
+
一个纯计算的死循环会让心跳线程长时间得不到调度——**心跳不再续期**,而框架看到的是
|
|
192
|
+
「这个任务没有心跳了」,于是按心跳超时处理(默认 30s)。任务其实好端端在算。
|
|
193
|
+
|
|
194
|
+
对策是**在关键点让出 GIL**:调一次 `self.report_progress(...)`(它做 Redis IO,天然让出),
|
|
195
|
+
或在长循环里周期性让渡:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
@xschedy_heartbeat_task(name="heavy_calc", timeout=600)
|
|
199
|
+
def heavy_calc(self, user_data):
|
|
200
|
+
for i, chunk in enumerate(chunks):
|
|
201
|
+
compute(chunk)
|
|
202
|
+
if i % 100 == 0:
|
|
203
|
+
self.report_progress(i, len(chunks)) # 保住心跳,顺带把进度送出去
|
|
204
|
+
return {"done": True}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> ⚠️ **还有一个不那么直观的连带后果**:`xschedy:idem:{key}`(幂等去重键)的 TTL 同样由心跳
|
|
208
|
+
> 线程每 tick 续期。所以**心跳一停,幂等窗口就开始倒计时**——同 key 的下发会在窗口过期后
|
|
209
|
+
> 重新产生一个新任务,去重形同失效。
|
|
210
|
+
|
|
211
|
+
> 顺带一提:**心跳检测不到「任务自己卡住」**——它只在「进程内的心跳线程不再跑」时才中断
|
|
212
|
+
> (进程没了 / 上面这条 GIL 饿死)。阻塞在锁、IO、死锁上的任务 GIL 是释放的,心跳照跑。
|
|
213
|
+
> 「卡住」的可见性由进度停滞指标 `xschedy_task_stalled` 提供。
|
|
214
|
+
|
|
215
|
+
### 对象存储(S3/MinIO)
|
|
216
|
+
|
|
217
|
+
worker 内置 S3/MinIO 客户端,业务任务可直接存/取对象(大结果不转存、超 `RESULT_MAX_BYTES` 直接报错;S3 是给业务的便捷封装):
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from xschedy.worker import get_s3
|
|
221
|
+
|
|
222
|
+
s3 = get_s3() # S3Client 单例(fork-safe 惰性初始化,同 get_redis)
|
|
223
|
+
|
|
224
|
+
s3.upload(b"bytes", key="x/y.txt", bucket="data") # 内存字节 → 返回 key
|
|
225
|
+
s3.upload_file("./report.pdf", key="reports/2026.pdf", bucket="data") # 单文件(流式,支持大文件)→ key
|
|
226
|
+
s3.upload_folder("./exports", prefix="exports/2026", bucket="data") # 递归上传目录 → 返回 key 列表
|
|
227
|
+
|
|
228
|
+
data = s3.download("x/y.txt", bucket="data") # → bytes
|
|
229
|
+
url = s3.presigned_url("x/y.txt", expires=3600, bucket="data") # 临时下载 URL
|
|
230
|
+
s3.exists("x/y.txt", bucket="data") # → bool
|
|
231
|
+
s3.set_tags("x/y.txt", {"status": "committed"}, bucket="data") # 对象标签(覆盖全部)
|
|
232
|
+
s3.remove_tags("x/y.txt", bucket="data") # 删除全部标签
|
|
233
|
+
s3.delete("x/y.txt", bucket="data") # 删除对象
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- `bucket` 必填(无全局默认);`upload` / `upload_file` / `upload_folder` 的 `tags` 可选(传了才打标签,默认不传)。
|
|
237
|
+
- 配置走 `XSCHEDY_S3_*`:`XSCHEDY_S3_ENDPOINT`(host:port)、`XSCHEDY_S3_ACCESS_KEY` / `XSCHEDY_S3_SECRET_KEY`、`XSCHEDY_S3_SECURE`(是否 HTTPS)、`XSCHEDY_S3_REGION`。
|
|
238
|
+
- 内置错误处理 + 重试:瞬态错误(5xx/限流/连接超时)指数退避重试,最终失败 re-raise(任务可见失败、走 Celery 重试)。
|
|
239
|
+
|
|
240
|
+
### Redis 直连 + 原子操作 / 分布式锁
|
|
241
|
+
|
|
242
|
+
worker 内置 Redis 客户端(复用 `result_backend`),业务任务可直接读写,并附一套原子操作 / 分布式锁工具:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from xschedy.worker import (
|
|
246
|
+
get_redis,
|
|
247
|
+
redis_incr_with_ttl, redis_incr_with_ttl_once, redis_decr_floor,
|
|
248
|
+
redis_hset_atomic, redis_sadd_atomic, redis_lpush_atomic,
|
|
249
|
+
redis_slot_acquire, redis_slot_release, redis_pool_acquire, redis_pool_release,
|
|
250
|
+
redis_lock, redis_acquire_lock, redis_release_lock, redis_renew_lock,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
r = get_redis() # Redis 客户端(零参数,复用 app 的 redis_* 配置)
|
|
254
|
+
r.set("k", "v"); r.get("k") # 原生 redis-py 命令直接可用
|
|
255
|
+
|
|
256
|
+
# 原子计数(Lua 保证「读-判断-写」原子)
|
|
257
|
+
redis_incr_with_ttl(r, "counter", 3600) # INCR + PEXPIRE(每次续期)
|
|
258
|
+
redis_incr_with_ttl_once(r, "limit", 86400) # 仅首次设 TTL(靠 TTL 兜底清零的计数,不续期)
|
|
259
|
+
# ⚠️ 并发限流**不用它**——见下方占槽原语 redis_slot_*
|
|
260
|
+
redis_decr_floor(r, "limit") # 带下限 DECR(>=1 才减,防减成负数)
|
|
261
|
+
|
|
262
|
+
# 原子「写入 + 过期」(单条 Lua)
|
|
263
|
+
redis_hset_atomic(r, "h", {"a": 1, "b": 2}, expire=3600)
|
|
264
|
+
redis_sadd_atomic(r, "s", "m1", "m2", expire=3600)
|
|
265
|
+
redis_lpush_atomic(r, "q", "v1", "v2", expire=3600)
|
|
266
|
+
|
|
267
|
+
# 占槽 / 占资源池(成员记归属,重复释放天然幂等)
|
|
268
|
+
redis_slot_acquire(r, "xschedy:concurrency:t", "task-id", 86400) # 占一个槽,返回占用数
|
|
269
|
+
redis_slot_release(r, "xschedy:concurrency:t", "task-id") # 还槽
|
|
270
|
+
redis_pool_acquire(r, "xschedy:pool:gpu", "task-id", 2, 4, 86400) # 占 2 槽(池容量 4);-1 = 超限
|
|
271
|
+
redis_pool_release(r, "xschedy:pool:gpu", "task-id") # 整份还回
|
|
272
|
+
|
|
273
|
+
# 分布式锁(context manager,可选自动续期看门狗)
|
|
274
|
+
with redis_lock(r, "job-lock", ttl=30):
|
|
275
|
+
...
|
|
276
|
+
# 手动抢 / 续 / 放
|
|
277
|
+
token = redis_acquire_lock(r, "job-lock", ttl=30)
|
|
278
|
+
redis_renew_lock(r, "job-lock", token, 30)
|
|
279
|
+
redis_release_lock(r, "job-lock", token)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
> `get_redis()` 零参数:内部复用 Celery app 的 `result_backend` + redis_* 韧性配置(socket_timeout=5s、连接池 50、health_check 30s)。fork 后子进程首次调用才建连接,避免继承父进程坏连接。
|
|
283
|
+
|
|
284
|
+
## 下发任务
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# HTTP
|
|
288
|
+
curl -X POST http://localhost:9100/api/tasks \
|
|
289
|
+
-H 'Content-Type: application/json' \
|
|
290
|
+
-d '{"function_name":"order_pay","queue_name":"my_queue","user_data":"{\"order_id\":123}"}'
|
|
291
|
+
|
|
292
|
+
# 查询 / 控制
|
|
293
|
+
GET /api/tasks # 列表(分页 + 筛选)
|
|
294
|
+
GET /api/tasks/{task_id} # 详情(三段 fallback:Redis 实时 → DB 历史 → 404)
|
|
295
|
+
POST /api/tasks/{task_id}/revoke?terminate=true # 撤销(强杀执行中;不带 = 软撤销排队中)
|
|
296
|
+
POST /api/tasks/{task_id}/relaunch # 重新执行(新 task_id)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
> ⚠️ **单字段的「请求体」都是查询参数**:`revoke?terminate=`、`toggle?enabled=`、
|
|
300
|
+
> `DELETE /api/queues/{n}/messages?confirm=`、`pool?grow=|shrink=`、`autoscale?min=&max=`、
|
|
301
|
+
> `rate-limit?task_name=&rate_limit=`、`queue/add-consumer?queue=`。顺带修掉了「DELETE 带 body」。
|
|
302
|
+
|
|
303
|
+
> **列表响应统一是信封**:集合键一律 `items`、计数一律 `total`、被硬上限截断
|
|
304
|
+
> 时 `truncated: true`;分页端点(`/api/tasks`、`/api/callbacks/dead-letter`)另有 `page`/`page_size`。
|
|
305
|
+
> 调用方**不必按端点记形状**——此前 `/api/workers` 用 `workers` 键、`/periodic-tasks` 用 `count`、
|
|
306
|
+
> `/groups` 是裸数组。CLI 的 `--format json` 用同一套信封。
|
|
307
|
+
|
|
308
|
+
下发请求关键字段:
|
|
309
|
+
> ⚠️ **字段名一律不带 `task_` 前缀**(已去掉)。旧名(`task_function_name` 等)现在会
|
|
310
|
+
> **422**——请求模型开了 `extra="forbid"`,「拼错/用旧名」都要报错而不是静默忽略。
|
|
311
|
+
|
|
312
|
+
- `function_name` / `queue_name`:任务名与下发队列(必填)
|
|
313
|
+
- `idempotent_key`:幂等键,同 key 在途时不重复下发
|
|
314
|
+
- `timeout_s`:心跳超时(默认走全局 `XSCHEDY_TASK_TIMEOUT_S`=30s)
|
|
315
|
+
- `queue_timeout_s`:排队超时(默认走全局 `XSCHEDY_QUEUE_TIMEOUT_S`=3 天,超时重下发 ≤2 次)
|
|
316
|
+
- `notify`:结果通知(`{"type":"webhook","config":{"url":"..."}}`)
|
|
317
|
+
|
|
318
|
+
## 回调通知
|
|
319
|
+
|
|
320
|
+
下发时带 `notify`,任务终态时由 callback worker 推送:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
notify = {
|
|
324
|
+
"type": "webhook", # webhook | mq | redis
|
|
325
|
+
"on_success": True,
|
|
326
|
+
"on_failure": True,
|
|
327
|
+
"config": {"url": "http://your-service/hook?token=xxx"},
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`config` 里的凭证用 Fernet 加密落库(`XSCHEDY_SECRET_KEY`)。推送失败自动重试(指数退避),耗尽进死信(`status=dead`)。
|
|
332
|
+
|
|
333
|
+
开关按「**决定结局的原因**」分三个(互斥且完备):`on_success`(默认开)、`on_failure`(默认开)、
|
|
334
|
+
`on_timeout`(默认关,指「因超时判定结束」——排队超时重发耗尽 / 心跳毒丸止损)。**一个任务只会成功推一条通知**:
|
|
335
|
+
`task_callback` 是 `task_id` 唯一的一行,推成功后后续入队条目会被静默丢弃。
|
|
336
|
+
|
|
337
|
+
进度推送(`on_progress`)走**独立通道**(`xschedy:progress_queue`,尽力而为、丢了可容忍),不占上面那唯一一次结果通知。
|
|
338
|
+
开启后**由 `report_progress` 在进度变化那一刻直接推**(不等心跳周期),**零配置**——推的时机由两条内建规则决定:
|
|
339
|
+
**进度真的变了**(同一个水位报第二遍没有信息量),且**距上次推送 ≥ 1 秒**
|
|
340
|
+
(`XSCHEDY_PROGRESS_PUSH_MIN_INTERVAL_S` 可全局调)。调用方不需要配节流。
|
|
341
|
+
|
|
342
|
+
SLA 违约、worker 掉线、进度停滞**不推通知**(它们不决定任务结局,占用那唯一一次机会会让真正的结局讲不出来)——
|
|
343
|
+
三者分别由通知 payload 的 `sla_missed` 字段、`error` 字段(掉线时写的就是它)、`xschedy_task_stalled` 指标承载。
|
|
344
|
+
**调度器自身没有第二条告警通道**——运维侧由 Prometheus 规则承载(`mixins/prometheus-alerts.yaml`),
|
|
345
|
+
调用方漏配通知不该由调度器替他决定要收什么。
|
|
346
|
+
|
|
347
|
+
## 关键配置(`XSCHEDY_*` 环境变量)
|
|
348
|
+
|
|
349
|
+
| 变量 | 默认 | 说明 |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| `BROKER_URL` | `amqp://localhost:5672` | RabbitMQ |
|
|
352
|
+
| `RESULT_BACKEND` | `redis://localhost:6379/1` | Redis |
|
|
353
|
+
| `DB_URL` | `postgresql+psycopg://localhost:5432/xschedy` | 历史落库(默认 PG) |
|
|
354
|
+
| `API_TOKEN` | 空(不认证) | 静态 token |
|
|
355
|
+
| `SECRET_KEY` | 空(明文) | 回调 config 加密密钥 |
|
|
356
|
+
| `MAX_QUEUE_DEPTH` | 10000 | 下发背压阈值 |
|
|
357
|
+
| `TASK_CONCURRENCY_LIMIT` | 空(=不限) | 按任务名的并发上限,形如 `{"task.a": 10}`;超限 429 |
|
|
358
|
+
| `TASK_POOL` | 空(=无池) | 命名资源池:几个任务共享一份下游配额,形如 `{"gpu": {"slots": 4, "tasks": {"task.a": 2}}}`;超限 429 |
|
|
359
|
+
| `BACKFILL_MAX_COUNT` | 100 | 单次补数最多投几个点 |
|
|
360
|
+
| `WORK_DIR` | `~/.xschedy` | 作业目录:机器本地产物的落点。日志默认写在其 `logs/` 下(`LOG_PATH` 可显式覆盖) |
|