xschedy 0.1.3__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.
- xschedy-0.1.3/.gitignore +28 -0
- xschedy-0.1.3/LICENSE +21 -0
- xschedy-0.1.3/PKG-INFO +419 -0
- xschedy-0.1.3/README.md +382 -0
- xschedy-0.1.3/pyproject.toml +110 -0
- xschedy-0.1.3/xschedy/__init__.py +13 -0
- xschedy-0.1.3/xschedy/cli.py +233 -0
- xschedy-0.1.3/xschedy/client.py +703 -0
- xschedy-0.1.3/xschedy/core/__init__.py +6 -0
- xschedy-0.1.3/xschedy/core/app.py +264 -0
- xschedy-0.1.3/xschedy/core/constants.py +240 -0
- xschedy-0.1.3/xschedy/core/file_cache.py +391 -0
- xschedy-0.1.3/xschedy/core/identity.py +30 -0
- xschedy-0.1.3/xschedy/core/logging.py +88 -0
- xschedy-0.1.3/xschedy/core/redis_store.py +1509 -0
- xschedy-0.1.3/xschedy/core/s3_client.py +460 -0
- xschedy-0.1.3/xschedy/core/states.py +639 -0
- xschedy-0.1.3/xschedy/core/threads.py +50 -0
- xschedy-0.1.3/xschedy/core/timeutil.py +94 -0
- xschedy-0.1.3/xschedy/core/utils.py +83 -0
- xschedy-0.1.3/xschedy/core/worker_core.py +934 -0
- xschedy-0.1.3/xschedy/worker/__init__.py +81 -0
xschedy-0.1.3/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
22
|
+
|
|
23
|
+
# 控制面容器 bind 出来的作业目录(日志在 deploy/work/logs)
|
|
24
|
+
deploy/xschedy/
|
|
25
|
+
deploy/xschedy-worker/
|
|
26
|
+
|
|
27
|
+
# Java 客户端构建产物
|
|
28
|
+
clients/java/target/
|
xschedy-0.1.3/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.3/PKG-INFO
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xschedy
|
|
3
|
+
Version: 0.1.3
|
|
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.7.0
|
|
19
|
+
Requires-Dist: loguru>=0.7
|
|
20
|
+
Requires-Dist: orjson>=3.12.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: redis>=6.2
|
|
23
|
+
Provides-Extra: client
|
|
24
|
+
Requires-Dist: httpx>=0.27; extra == 'client'
|
|
25
|
+
Provides-Extra: server
|
|
26
|
+
Requires-Dist: croniter>=2.0; extra == 'server'
|
|
27
|
+
Requires-Dist: cryptography>=42.0; extra == 'server'
|
|
28
|
+
Requires-Dist: fastapi>=0.111; extra == 'server'
|
|
29
|
+
Requires-Dist: httpx>=0.27; extra == 'server'
|
|
30
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'server'
|
|
31
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'server'
|
|
32
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'server'
|
|
33
|
+
Requires-Dist: uvicorn[standard]>=0.30; extra == 'server'
|
|
34
|
+
Provides-Extra: worker
|
|
35
|
+
Requires-Dist: httpx>=0.27; extra == 'worker'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Xschedy
|
|
39
|
+
|
|
40
|
+
在 Celery 之上的一组业务封装:用装饰器写任务,用 `xschedy worker` 运行,用 Python SDK 下发与查询。
|
|
41
|
+
|
|
42
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install xschedy[worker] # 执行面:worker + 调用方 SDK(含 httpx)
|
|
46
|
+
pip install xschedy[client] # 只装调用方 SDK(核心 + httpx)
|
|
47
|
+
pip install xschedy # 仅核心(不含 httpx;不用 SDK / submit_task 时)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 快速开始
|
|
51
|
+
|
|
52
|
+
**1. 写任务**(`myproject/tasks.py`):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from xschedy.worker import TaskContext, xschedy_heartbeat_task
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@xschedy_heartbeat_task(name="order_pay")
|
|
59
|
+
def order_pay(self, ctx: TaskContext):
|
|
60
|
+
return {"paid": True}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**2. 启动 worker**:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
xschedy worker -Q default --tasks myproject.tasks
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**3. 下发并等结果**:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from xschedy.client import SyncClient
|
|
73
|
+
|
|
74
|
+
with SyncClient("http://localhost:9100", token="…") as c:
|
|
75
|
+
sub = c.submit("order_pay", "default", user_data='{"order_id": 1}')
|
|
76
|
+
view = c.wait(sub.task_id, timeout=120)
|
|
77
|
+
print(view.status.value, view.result)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 写业务任务
|
|
81
|
+
|
|
82
|
+
任务命名 `项目.模块.函数`,用 `@xschedy_heartbeat_task` 注册;worker 启动时用 `--tasks` 加载定义任务的模块(逗号分隔多个)。
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from xschedy.worker import TaskContext, xschedy_heartbeat_task
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@xschedy_heartbeat_task(
|
|
89
|
+
name="order_pay",
|
|
90
|
+
heartbeat_interval=2.0, # 心跳间隔
|
|
91
|
+
timeout=600, # 硬超时(秒)
|
|
92
|
+
soft_timeout=540, # 软超时(<= timeout)
|
|
93
|
+
max_retries=3, # 重试次数
|
|
94
|
+
retry_backoff=True, # 指数退避
|
|
95
|
+
abortable=True, # 允许协作式中止
|
|
96
|
+
)
|
|
97
|
+
def order_pay(self, ctx: TaskContext):
|
|
98
|
+
self.report_progress(current=1, total=3, description="扣款")
|
|
99
|
+
return {"paid": True}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 业务函数参数
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
def f(self, ctx: TaskContext):
|
|
106
|
+
...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `ctx.user_data`:下发时给的 JSON **字符串原样**(`str | None`),框架不解析——要 dict 就 `json.loads`,要模型就 `model_validate_json`。
|
|
110
|
+
- `self.request.id`:task_id;`self.request.retries`:第几次尝试(0 起)。
|
|
111
|
+
- `self.request.hostname` / `delivery_info` / `headers`:Celery 原生信息。
|
|
112
|
+
|
|
113
|
+
框架字段都在 `ctx` 上:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
ctx.task_name # 任务名
|
|
117
|
+
ctx.scheduled_time # 定时任务的计划触发时刻(手动下发为 None)
|
|
118
|
+
ctx.default_args # XSCHEDY_DEFAULT_ARGS(透传不合并)
|
|
119
|
+
ctx.periodic_kwargs # 定时任务 kwargs(透传不合并)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 进度与中止
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
self.report_progress(current, total, description=None, extra=None) # 自动算 percent + 剩余时间
|
|
126
|
+
self.increment_progress(by=1, description=None, extra=None) # 不知道 total 时递增
|
|
127
|
+
self.is_aborted() # 需 abortable=True
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `extra` 是你自定义的**不透明字符串**(要结构化就自己 `json.dumps`),随进度返回给调用方;`description` / `extra` 各自 64KB 封顶,超限按 UTF-8 截断并留痕。
|
|
131
|
+
- `increment_progress` 的 `description` / `extra` 是**覆盖**语义:不传即清空;`total` 不会被抹掉。
|
|
132
|
+
- `is_aborted()` 只在 `abortable=True` 时有效,且在关键点主动调用才有用(阻塞 IO 期间无法响应)。未开启时恒 `False`,并打一次告警。
|
|
133
|
+
|
|
134
|
+
### 生命周期钩子(零 celery import)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from xschedy.worker import xschedy_worker_process_init, xschedy_worker_process_shutdown
|
|
138
|
+
|
|
139
|
+
@xschedy_worker_process_init
|
|
140
|
+
def init_process():
|
|
141
|
+
global engine
|
|
142
|
+
engine = create_engine(DB_URL) # 连接类资源必须建在子进程钩子里
|
|
143
|
+
|
|
144
|
+
@xschedy_worker_process_shutdown
|
|
145
|
+
def close_process(pid, exitcode):
|
|
146
|
+
engine.dispose()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| 钩子 | 时机 |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `@xschedy_worker_init` | 主进程启动、fork 之前 |
|
|
152
|
+
| `@xschedy_worker_process_init` | 每个子进程启动 |
|
|
153
|
+
| `@xschedy_worker_process_shutdown` | 子进程退出前 |
|
|
154
|
+
| `@xschedy_worker_shutdown` | 主进程关闭 |
|
|
155
|
+
|
|
156
|
+
### 日志
|
|
157
|
+
|
|
158
|
+
直接使用框架的 logger,不要自己初始化 loguru:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from xschedy.worker import logger
|
|
162
|
+
|
|
163
|
+
logger.info("order %s paid", order_id) # 行内自动带 task_id
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
日志同时写 stderr 与 `<XSCHEDY_WORK_DIR>/logs/`(按天轮转,保留 `XSCHEDY_LOG_SAVE_DAY` 天)。
|
|
167
|
+
|
|
168
|
+
### 三条必须知道的约束
|
|
169
|
+
|
|
170
|
+
**① 不要把凭证放进 `user_data`。** 它会明文落库、也会进 worker 日志,框架不做脱敏。需要传递就传引用(例如密钥管理系统里的 key 名)。`user_define` 同理,而且它随每次进度通知重发一遍,只适合放短期票据。
|
|
171
|
+
|
|
172
|
+
**② 等外部事件不要在 worker 里 `sleep` 轮询。** 用 `self.retry(countdown=N)` 把任务重新入队,释放执行槽:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
if not upstream_ready(job_id):
|
|
176
|
+
self.retry(countdown=60)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**③ CPU 密集任务要在关键点让出 GIL。** 心跳跑在独立线程里;纯计算不释放 GIL 会让心跳停摆,任务被误判超时。周期性调一次 `self.report_progress(...)`(顺带推送进度)即可:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
for i, chunk in enumerate(chunks):
|
|
183
|
+
compute(chunk)
|
|
184
|
+
if i % 100 == 0:
|
|
185
|
+
self.report_progress(i, len(chunks))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> 心跳一停,幂等键 `xschedy:idem:{key}` 也不再续期,同 key 的下发会在窗口过期后重新建任务。
|
|
189
|
+
> 另外,心跳检测不到「任务自己阻塞在锁/IO 上」——那类停滞由 `xschedy_task_stalled` 指标反映。
|
|
190
|
+
|
|
191
|
+
## 运行 worker
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# 普通
|
|
195
|
+
xschedy worker -Q my_queue --tasks myproject.tasks
|
|
196
|
+
|
|
197
|
+
# 分片:监听 {queue}.shard.N;下发方带 shard_key 时按 md5(key) % N 路由
|
|
198
|
+
xschedy worker -Q my_queue.shard.0,my_queue.shard.1 --tasks myproject.tasks
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- **`--tasks` 必须给**。漏了不会报错,只打一条 WARNING,但一个任务都不会注册,下发会得到 `UNKNOWN_TASK`。
|
|
202
|
+
- **同机多 worker 必须给互不相同的 `--name`**:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
xschedy worker -n worker1 -Q q1 --tasks myproject.tasks
|
|
206
|
+
xschedy worker -n worker2 -Q q2 --tasks myproject.tasks
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
名字里**不要带 `@` 或 `.`**:`-n worker1` 正确;`-n w1@host1` 会取 `host1`,与 `-n w2@host1` 撞成同一个 worker;不传则所有进程都是 `celery@<本机hostname>`。
|
|
210
|
+
- 分片没有 worker 监听时,带 `shard_key` 的任务退回普通队列(并打 WARNING);分片数变化会让几乎所有 key 重新分布,不建议在有顺序性诉求的 key 上频繁扩缩容。
|
|
211
|
+
|
|
212
|
+
## 调用任务(`xschedy.client`)
|
|
213
|
+
|
|
214
|
+
`Client` 是异步版,`SyncClient` 是同步版,方法与参数一一对应:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import asyncio
|
|
218
|
+
from xschedy.client import Client, XschedyError
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def main():
|
|
222
|
+
async with Client("http://localhost:9100", token="…") as c:
|
|
223
|
+
sub = await c.submit("order_pay", "default", user_data='{"order_id": 1}')
|
|
224
|
+
view = await c.wait(sub.task_id, timeout=120)
|
|
225
|
+
print(view.status.value, view.result)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
asyncio.run(main())
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from xschedy.client import SyncClient
|
|
233
|
+
|
|
234
|
+
with SyncClient("http://localhost:9100", token="…") as c:
|
|
235
|
+
view = c.submit_sync("order_pay", "default", user_data='{"order_id": 1}')
|
|
236
|
+
print(view.status.value, view.result)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`submit` 的其余关键字参数与请求字段同名:`user_data`、`tag`、`priority`、`idempotent_key`、`timeout_s`、`queue_timeout_s`、`max_requeue`、`delay_run_s`、`notify`、`deadline` 等。`submit` 返回 `Submission`(`task_id` / `deduplicated` / `task`)。**`mode` 不走这个自由字段**:同步下发请用 `submit_sync`,传 `mode=` 会直接报错。
|
|
240
|
+
|
|
241
|
+
常用方法:
|
|
242
|
+
|
|
243
|
+
| 方法 | 作用 |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `submit` / `submit_sync` | 异步下发 / 阻塞到终态 |
|
|
246
|
+
| `get(task_id)` | 查询最新视图 |
|
|
247
|
+
| `wait(task_id, timeout=…)` | 轮询到终态;超时抛 `WAIT_TIMEOUT`,不 revoke 任务 |
|
|
248
|
+
| `retry` / `revoke(terminate=)` / `abort` / `relaunch` | 同 task_id 重试 / 撤销 / 协作式中止 / 新 id 重发 |
|
|
249
|
+
|
|
250
|
+
错误统一抛 `XschedyError`,带 `code` / `message` / `status_code` / `task_id`。
|
|
251
|
+
|
|
252
|
+
重试策略(`retries=N`,默认 0):
|
|
253
|
+
|
|
254
|
+
- `get` / `wait` 对连接错误与 429 / 502 / 503 / 504 重试;
|
|
255
|
+
- `submit` **只有带 `idempotent_key` 时**才重试(避免建出两条任务);
|
|
256
|
+
- `submit_sync` 不自动重试。
|
|
257
|
+
|
|
258
|
+
### 管理方法(同一个客户端)
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
with SyncClient("http://localhost:9100", token="…") as c:
|
|
262
|
+
c.list_tasks(status="failed", page_size=50)
|
|
263
|
+
c.inflight_tasks(queue="order")
|
|
264
|
+
c.attempts(task_id)
|
|
265
|
+
c.get_raw(task_id) # result 的 backend 原样值
|
|
266
|
+
c.retry_many(["t1", "t2"], dry_run=True)
|
|
267
|
+
c.delete_many({"tag": "batch-1"}, dry_run=True)
|
|
268
|
+
c.revoke_many({"tag": "batch-1"}, confirm=True) # 真撤销要显式 confirm
|
|
269
|
+
c.abort_many({"tag": "batch-1"}, dry_run=True)
|
|
270
|
+
c.delete(task_id)
|
|
271
|
+
c.task_types()
|
|
272
|
+
c.set_timeout("file.parse", soft=540, hard=600)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
批量动作的筛选字段与 `list_tasks` 相同(status/queue/name/tag/worker/dispatch_reason/时间范围),至少给一个条件;`dry_run=True` 只预览,真执行要 `confirm=True`。
|
|
276
|
+
|
|
277
|
+
### 任务内发子任务
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
@xschedy_heartbeat_task(name="rag_parse")
|
|
281
|
+
def rag_parse(self, ctx: TaskContext):
|
|
282
|
+
ctx.submit_task(
|
|
283
|
+
"rag_chunk", "rag",
|
|
284
|
+
user_data=json.dumps({"doc_id": doc_id}),
|
|
285
|
+
idempotent_key=f"{doc_id}:chunk:v1",
|
|
286
|
+
)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
地址与 token 默认取 `XSCHEDY_API_URL` / `XSCHEDY_API_TOKEN`,也可以 `submit_task(..., base_url=..., token=...)` 单次覆盖。子任务请显式给幂等键,父任务重发时才不会重复下发。
|
|
290
|
+
|
|
291
|
+
## 结果通知(可选)
|
|
292
|
+
|
|
293
|
+
下发时带 `notify` 才会推送;不填则一条都不推。
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
c.submit(
|
|
297
|
+
"order_pay", "default",
|
|
298
|
+
notify={
|
|
299
|
+
"type": "webhook", # webhook | mq | redis
|
|
300
|
+
"on_success": True,
|
|
301
|
+
"on_failure": True,
|
|
302
|
+
"on_timeout": True, # 默认 False
|
|
303
|
+
"config": {"url": "https://example.com/hook"},
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
通知体只有两种,靠 `frame_type` 区分,`task` 结构相同:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"frame_type": "terminal",
|
|
313
|
+
"task": {"task_id": "…", "status": "success", "result": {"ok": true}, "error": null, ...},
|
|
314
|
+
"execution_id": "…",
|
|
315
|
+
"callback_attempt": 1,
|
|
316
|
+
"notify_type": "webhook",
|
|
317
|
+
"progress_at": null
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- 终态通知每个任务**只有一条**;`on_success` / `on_failure` 默认开,`on_timeout` 默认关(因超时结束的任务要显式打开)。
|
|
322
|
+
- at-least-once:接收方按 `task.task_id` + `execution_id` + `callback_attempt` 去重。
|
|
323
|
+
- `notify.on_progress=True` 时另推进度帧(`frame_type="progress"`),内容同形;进度是水位,同一个值不会重复推。
|
|
324
|
+
- `type=mq` 只向 exchange + routing_key 投递,**消费方要先建好队列并绑定**,否则消息会被 broker 丢弃。
|
|
325
|
+
- 配置了 `XSCHEDY_SECRET_KEY` 时,`notify.config` 里的凭证会加密存储;日志与响应始终脱敏。
|
|
326
|
+
|
|
327
|
+
## 对象存储(S3 / MinIO)
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from xschedy.worker import get_s3
|
|
331
|
+
|
|
332
|
+
s3 = get_s3()
|
|
333
|
+
s3.upload(b"bytes", key="x/y.txt", bucket="data")
|
|
334
|
+
s3.upload_file("./report.pdf", key="reports/2026.pdf", bucket="data", with_hash_tag=True)
|
|
335
|
+
s3.upload_folder("./exports", prefix="exports/2026", bucket="data")
|
|
336
|
+
|
|
337
|
+
data = s3.download("x/y.txt", bucket="data") # 小对象,整份进内存
|
|
338
|
+
path = s3.download_to_file("big.bin", bucket="data") # 大文件,流式落盘
|
|
339
|
+
path = s3.download_to_file("big.bin", "./tmp/big.bin", bucket="data") # 指定落点
|
|
340
|
+
url = s3.presigned_url("x/y.txt", expires=3600, bucket="data")
|
|
341
|
+
s3.exists("x/y.txt", bucket="data")
|
|
342
|
+
s3.get_tags("x/y.txt", bucket="data")
|
|
343
|
+
s3.set_tags("x/y.txt", {"status": "committed"}, bucket="data")
|
|
344
|
+
s3.delete("x/y.txt", bucket="data")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- `bucket` 必填。
|
|
348
|
+
- `download` 适合小对象;大文件用 `download_to_file`(临时文件 + 原子改名,失败不留半截)。省略 `path` 时落到缓存目录。
|
|
349
|
+
- `with_hash_tag=True` 会把整文件摘要写进对象 tag(键 `digest`,值 `sha256:<hex>`);代价是多读一遍文件。
|
|
350
|
+
|
|
351
|
+
## 文件缓存(`get_cached_file`)
|
|
352
|
+
|
|
353
|
+
同一个对象要反复读,或要交给 pandas / ffmpeg 这类按路径读文件的库:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
from xschedy.worker import FileIntegrityError, get_cached_file
|
|
357
|
+
|
|
358
|
+
path = get_cached_file("data", "orders/2026/11/x.csv") # -> pathlib.Path
|
|
359
|
+
df = pandas.read_csv(path)
|
|
360
|
+
|
|
361
|
+
path = get_cached_file("data", "big.bin", digest="9e107d…", verify=True)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- 落点:`<XSCHEDY_WORK_DIR>/cache/<bucket>/<key>`,文件名照搬对象 key。
|
|
365
|
+
- 命中判据是两把 SHA-256 摘要相等:远端对象 tag 的 `digest` 与本地文件摘要;远端没有 tag 时按 size+mtime 命中并告警一次;下载后不符抛 `FileIntegrityError`,半成品不入缓存。
|
|
366
|
+
- 上限 `XSCHEDY_FILE_CACHE_MAX_MB`(默认 10240)与 `XSCHEDY_FILE_CACHE_MAX_FILES`(默认 1000),超限按 LRU 淘汰;关掉 `XSCHEDY_FILE_CACHE_ENABLED` 则每次都下载覆盖。
|
|
367
|
+
- 上传时打 tag(`with_hash_tag=True`)才能做内容校验。
|
|
368
|
+
|
|
369
|
+
## Redis 直连与原子操作
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from xschedy.worker import (
|
|
373
|
+
get_redis,
|
|
374
|
+
redis_incr_with_ttl, redis_incr_with_ttl_once, redis_decr_floor,
|
|
375
|
+
redis_hset_atomic, redis_sadd_atomic, redis_lpush_atomic,
|
|
376
|
+
redis_lock, redis_acquire_lock, redis_renew_lock, redis_release_lock,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
r = get_redis() # 复用 worker 的 Redis 配置
|
|
380
|
+
r.set("k", "v"); r.get("k") # 原生 redis-py 命令可用
|
|
381
|
+
|
|
382
|
+
redis_incr_with_ttl(r, "counter", 3600) # INCR + 每次续期
|
|
383
|
+
redis_incr_with_ttl_once(r, "limit", 86400) # 仅首次设 TTL
|
|
384
|
+
redis_decr_floor(r, "limit") # 带下限 DECR
|
|
385
|
+
|
|
386
|
+
redis_hset_atomic(r, "h", {"a": 1}, expire=3600)
|
|
387
|
+
redis_sadd_atomic(r, "s", "m1", "m2", expire=3600)
|
|
388
|
+
redis_lpush_atomic(r, "q", "v1", "v2", expire=3600)
|
|
389
|
+
|
|
390
|
+
with redis_lock(r, "job-lock", ttl=30):
|
|
391
|
+
...
|
|
392
|
+
|
|
393
|
+
token = redis_acquire_lock(r, "job-lock", ttl=30)
|
|
394
|
+
redis_renew_lock(r, "job-lock", token, 30)
|
|
395
|
+
redis_release_lock(r, "job-lock", token)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
并发槽与命名资源池由框架自己管,不对业务开放。
|
|
399
|
+
|
|
400
|
+
## 配置
|
|
401
|
+
|
|
402
|
+
环境变量统一 `XSCHEDY_` 前缀,**进程启动时读取**,改完要重启:
|
|
403
|
+
|
|
404
|
+
| 变量 | 默认 | 说明 |
|
|
405
|
+
|---|---|---|
|
|
406
|
+
| `XSCHEDY_BROKER_URL` | `amqp://localhost:5672` | RabbitMQ |
|
|
407
|
+
| `XSCHEDY_RESULT_BACKEND` | `redis://localhost:6379/1` | Redis 结果后端 |
|
|
408
|
+
| `XSCHEDY_WORK_DIR` | `~/.xschedy` | 作业目录(日志、文件缓存) |
|
|
409
|
+
| `XSCHEDY_LOG_LEVEL` / `_PATH` / `_SAVE_DAY` | `INFO` / `<work>/logs` / `7` | 日志 |
|
|
410
|
+
| `XSCHEDY_HEARTBEAT_INTERVAL` | `5.0` | 默认心跳间隔(装饰器可覆盖) |
|
|
411
|
+
| `XSCHEDY_TASK_TIMEOUT_S` | `30.0` | 默认心跳超时(装饰器可覆盖) |
|
|
412
|
+
| `XSCHEDY_API_URL` / `_API_TOKEN` | `http://localhost:9100` / 空 | 任务内 `ctx.submit_task` 的地址与 token |
|
|
413
|
+
| `XSCHEDY_DEFAULT_ARGS` | 空 | 全局默认参数 JSON,透传不合并 |
|
|
414
|
+
| `XSCHEDY_S3_ENDPOINT` / `_ACCESS_KEY` / `_SECRET_KEY` / `_SECURE` / `_REGION` | `localhost:9000` / 空 / 空 / `false` / `us-east-1` | 对象存储 |
|
|
415
|
+
| `XSCHEDY_S3_HASH_TAG` | `digest` | 对象摘要 tag 的键名 |
|
|
416
|
+
| `XSCHEDY_FILE_CACHE_ENABLED` / `_MAX_MB` / `_MAX_FILES` | `true` / `10240` / `1000` | 文件缓存 |
|
|
417
|
+
| `XSCHEDY_TIMEZONE` | `Asia/Shanghai` | 定时任务与时间展示 |
|
|
418
|
+
|
|
419
|
+
> 变量名必须带 `XSCHEDY_` 前缀;少写前缀会被静默忽略并退回默认值。
|