nb-cron-nb 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.
Files changed (61) hide show
  1. nb_cron_nb-0.1.0/PKG-INFO +1276 -0
  2. nb_cron_nb-0.1.0/README.md +1245 -0
  3. nb_cron_nb-0.1.0/nb_cron/__init__.py +33 -0
  4. nb_cron_nb-0.1.0/nb_cron/_version.py +1 -0
  5. nb_cron_nb-0.1.0/nb_cron/api/__init__.py +0 -0
  6. nb_cron_nb-0.1.0/nb_cron/api/django_app.py +67 -0
  7. nb_cron_nb-0.1.0/nb_cron/api/fastapi_app.py +70 -0
  8. nb_cron_nb-0.1.0/nb_cron/api/flask_app.py +69 -0
  9. nb_cron_nb-0.1.0/nb_cron/api/handlers.py +148 -0
  10. nb_cron_nb-0.1.0/nb_cron/api/schemas.py +74 -0
  11. nb_cron_nb-0.1.0/nb_cron/core/__init__.py +0 -0
  12. nb_cron_nb-0.1.0/nb_cron/core/job.py +95 -0
  13. nb_cron_nb-0.1.0/nb_cron/core/registry.py +85 -0
  14. nb_cron_nb-0.1.0/nb_cron/core/scheduler.py +506 -0
  15. nb_cron_nb-0.1.0/nb_cron/cron_utils/__init__.py +4 -0
  16. nb_cron_nb-0.1.0/nb_cron/cron_utils/parser.py +45 -0
  17. nb_cron_nb-0.1.0/nb_cron/cron_utils/translator.py +302 -0
  18. nb_cron_nb-0.1.0/nb_cron/executors/__init__.py +0 -0
  19. nb_cron_nb-0.1.0/nb_cron/executors/base_executor.py +66 -0
  20. nb_cron_nb-0.1.0/nb_cron/executors/funboost_executor.py +238 -0
  21. nb_cron_nb-0.1.0/nb_cron/executors/thread_executor.py +33 -0
  22. nb_cron_nb-0.1.0/nb_cron/executors/threadpool.py +119 -0
  23. nb_cron_nb-0.1.0/nb_cron/locks/__init__.py +3 -0
  24. nb_cron_nb-0.1.0/nb_cron/locks/base.py +19 -0
  25. nb_cron_nb-0.1.0/nb_cron/locks/memory_lock.py +33 -0
  26. nb_cron_nb-0.1.0/nb_cron/locks/mongo_lock.py +51 -0
  27. nb_cron_nb-0.1.0/nb_cron/locks/redis_lock.py +41 -0
  28. nb_cron_nb-0.1.0/nb_cron/locks/sqlalchemy_lock.py +79 -0
  29. nb_cron_nb-0.1.0/nb_cron/metrics/__init__.py +0 -0
  30. nb_cron_nb-0.1.0/nb_cron/metrics/collector.py +136 -0
  31. nb_cron_nb-0.1.0/nb_cron/stores/__init__.py +3 -0
  32. nb_cron_nb-0.1.0/nb_cron/stores/base.py +67 -0
  33. nb_cron_nb-0.1.0/nb_cron/stores/memory.py +81 -0
  34. nb_cron_nb-0.1.0/nb_cron/stores/mongo_store.py +107 -0
  35. nb_cron_nb-0.1.0/nb_cron/stores/redis_store.py +108 -0
  36. nb_cron_nb-0.1.0/nb_cron/stores/sqlalchemy_store.py +251 -0
  37. nb_cron_nb-0.1.0/nb_cron/triggers/__init__.py +6 -0
  38. nb_cron_nb-0.1.0/nb_cron/triggers/base.py +22 -0
  39. nb_cron_nb-0.1.0/nb_cron/triggers/cron_trigger.py +89 -0
  40. nb_cron_nb-0.1.0/nb_cron/triggers/date_trigger.py +38 -0
  41. nb_cron_nb-0.1.0/nb_cron/triggers/interval_trigger.py +97 -0
  42. nb_cron_nb-0.1.0/nb_cron/utils.py +50 -0
  43. nb_cron_nb-0.1.0/nb_cron/web/__init__.py +3 -0
  44. nb_cron_nb-0.1.0/nb_cron/web/app.py +200 -0
  45. nb_cron_nb-0.1.0/nb_cron/web/static/assets/CronTool-CEbSw-3y.js +1 -0
  46. nb_cron_nb-0.1.0/nb_cron/web/static/assets/Dashboard-BbVnKYZ4.js +1 -0
  47. nb_cron_nb-0.1.0/nb_cron/web/static/assets/JobDetail-9EOvbHwG.js +1 -0
  48. nb_cron_nb-0.1.0/nb_cron/web/static/assets/JobList-Cz1caiIS.js +1 -0
  49. nb_cron_nb-0.1.0/nb_cron/web/static/assets/index-Bb6yjXMn.js +60 -0
  50. nb_cron_nb-0.1.0/nb_cron/web/static/assets/index-BrAU9wIe.js +118 -0
  51. nb_cron_nb-0.1.0/nb_cron/web/static/assets/index-CfX4R3Au.css +1 -0
  52. nb_cron_nb-0.1.0/nb_cron/web/static/favicon.svg +19 -0
  53. nb_cron_nb-0.1.0/nb_cron/web/static/index.html +14 -0
  54. nb_cron_nb-0.1.0/nb_cron/web/static_mime.py +44 -0
  55. nb_cron_nb-0.1.0/nb_cron_nb.egg-info/PKG-INFO +1276 -0
  56. nb_cron_nb-0.1.0/nb_cron_nb.egg-info/SOURCES.txt +59 -0
  57. nb_cron_nb-0.1.0/nb_cron_nb.egg-info/dependency_links.txt +1 -0
  58. nb_cron_nb-0.1.0/nb_cron_nb.egg-info/requires.txt +30 -0
  59. nb_cron_nb-0.1.0/nb_cron_nb.egg-info/top_level.txt +2 -0
  60. nb_cron_nb-0.1.0/pyproject.toml +58 -0
  61. nb_cron_nb-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,1276 @@
1
+ Metadata-Version: 2.1
2
+ Name: nb_cron_nb
3
+ Version: 0.1.0
4
+ Summary: A powerful and simple cron job scheduler that dominates APScheduler
5
+ Author: ydf0509
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ydf0509/nb_cron
8
+ Project-URL: Repository, https://github.com/ydf0509/nb_cron
9
+ Keywords: cron,scheduler,distributed,job,task
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Requires-Python: >=3.7
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: redis
24
+ Provides-Extra: mongo
25
+ Provides-Extra: sqlalchemy
26
+ Provides-Extra: fastapi
27
+ Provides-Extra: flask
28
+ Provides-Extra: django
29
+ Provides-Extra: all
30
+ Provides-Extra: dev
31
+
32
+
33
+ # 🚀 nb_cron
34
+
35
+ **下一代 Python 分布式定时任务框架(全面超越 APScheduler)**
36
+
37
+ nb_cron 是一个强大、极简且**专为云原生架构设计**的定时任务调度库。它不仅彻底解决了 APScheduler 常年存在的序列化崩溃、多实例重复执行等痛点,更在架构理念上实现了**“业务逻辑与调度配置的物理隔离”**,让 Python 定时任务的管理进入真正的现代化阶段。
38
+
39
+ ### 🥊 核心痛点对决:APScheduler vs nb_cron
40
+
41
+ | 痛点场景 | 😭 APScheduler 的历史包袱 | 🤩 nb_cron 的现代解决方案 |
42
+ | :--- | :--- | :--- |
43
+ | **云原生多副本部署**<br>*(K8s/多容器)* | 致命弱点:多实例会**重复执行任务**,需手动硬编码第三方 Redis 锁,极易死锁。 | **天生云原生**:内置极其可靠的分布式锁(Redis/Mongo/SQL),天然支持 K8s 多副本平滑扩容,保证 **exactly-once**(绝不重复执行)。 |
44
+ | **微服务跨项目调度** | 强耦合:任务代码和调度器必须在同一个项目中,无法集中化管理。 | **跨 Git 项目可视化编排**:首创业务与调度解耦机制。A 项目只写函数,B 项目(调度中心)通过 Web UI 动态下发定时配置。 |
45
+ | **代码重构与序列化** | Pickle 地狱:存入 DB 的是函数内存地址,一旦代码重构(改名/移动文件路径),反序列化直接崩溃,任务全线瘫痪。 | **彻底抛弃 Pickle**:首创 `@cron_register` 稳定名称注册表,存入 DB 的仅是纯字符串。代码随便重构,只要名字在,任务照样跑。 |
46
+ | **可视化管理后台** | 官方**没有 UI**,想要启停任务、看日志只能自己从头手搓前后端。 | **原生自带 Web UI**:内置开箱即用的 Vue3 + Element Plus 现代化后台。**前端已预编译到 `nb_cron/web`,Python 开发者无需安装 Node.js 即可一键启动。** |
47
+ | **精度与时区** | Cron 不支持秒级精度;时区配置与 Misfire 策略行为混乱。 | **强制 6 字段 Cron**(支持秒级);默认本地时区(可传 `tz`),Misfire 容忍策略极简可控。 |
48
+ | **多项目共享存储** | 多项目共用一个 Redis 时极易发生 Key 冲突,互相踩踏任务。 | **强制物理隔离**:初始化必须传 `name` 参数,按项目名称进行 Redis Key 的绝对隔离。 |
49
+ | **选择困难症** | 提供 7 种 Scheduler 类(Blocking, Background, AsyncIO...),新手永远选错。 | **大道至简**:全局只有一个 `NbCron` 类,永远在后台非阻塞运行,同时兼容同步与 `async` 异步函数。 |
50
+ | **集群分布式消费** | 只能在本地调度并执行,面对海量重计算任务力不从心。 | **一键变身分布式 MQ**:支持无缝切换至 `FunboostExecutor`,瞬间获得**失败重试、指数退避、超时杀死**等工业级分布式消息队列消费能力。 |
51
+
52
+ ### ✨ 为什么 nb_cron 是“下一代”框架?
53
+
54
+ 传统的定时任务框架,往往把**“业务代码”**和**“定时规则(如每天凌晨两点)”**死死绑在一起。
55
+ nb_cron 带来了全新的架构理念:
56
+
57
+ 1. **配置即数据,无需重启服务**:通过 Web UI 随时修改任务的 Cron 表达式或启停任务,配置实时写入 Redis 并生效,你的业务进程**全程无需重启**。
58
+ 2. **函数定义与任务调度的物理分离**:让后端开发人员只管专心写业务函数并打上 `@cron_register` 标记;让运维或运营人员在独立的 Web 页面上,通过下拉菜单选择已注册的函数,在nb_cron_ui 的前端去创建定时任务。
59
+
60
+
61
+ ## 安装
62
+
63
+ ```bash
64
+ # 基础安装(内存存储)
65
+ pip install nb_cron
66
+
67
+ # Redis 存储 + FastAPI Web(推荐,生产环境一行搞定)
68
+ pip install nb_cron[redis,fastapi]
69
+
70
+ # Redis + Flask
71
+ pip install nb_cron[redis,flask]
72
+
73
+ # 全部功能
74
+ pip install nb_cron[all]
75
+ ```
76
+
77
+ 各可选组件:
78
+
79
+ | 组件 | 安装命令 | 说明 |
80
+ |---|---|---|
81
+ | redis | `pip install nb_cron[redis]` | Redis 存储 + 分布式锁 |
82
+ | mongo | `pip install nb_cron[mongo]` | MongoDB 存储 + 分布式锁 |
83
+ | sqlalchemy | `pip install nb_cron[sqlalchemy]` | SQLite/MySQL/PostgreSQL 存储 |
84
+ | fastapi | `pip install nb_cron[fastapi]` | FastAPI Web 框架集成 |
85
+ | flask | `pip install nb_cron[flask]` | Flask Web 框架集成 |
86
+ | django | `pip install nb_cron[django]` | Django + Ninja 框架集成 |
87
+
88
+ ## 快速开始
89
+
90
+ ### 最简用法
91
+
92
+ ```python
93
+ from nb_cron import NbCron, cron_register
94
+
95
+ cron = NbCron("my_project") # name 必传,隔离不同项目
96
+
97
+ @cron.job("0 */5 * * * *") # 每5分钟执行(6字段:秒 分 时 日 月 周)
98
+ @cron_register('my_task') # 必须注册稳定名称
99
+ def my_task():
100
+ print("Hello nb_cron!")
101
+
102
+ cron.start() # 不阻塞,定时任务后台运行,进程不会退出
103
+ ```
104
+
105
+ ### 完整示例
106
+
107
+ ```python
108
+ from datetime import timedelta, timezone
109
+ from nb_cron import NbCron, cron_register, add_cron_register, explain_cron
110
+
111
+ # 创建调度器(name 必传,选一个存储后端)
112
+ cron = NbCron("my_project") # 内存存储
113
+ # cron = NbCron("my_project", "redis://localhost:6379/0") # Redis(推荐)
114
+ # cron = NbCron("my_project", tz=timezone(timedelta(hours=8))) # 指定东八区
115
+ # cron = NbCron("my_project", tz=timezone.utc) # UTC
116
+
117
+ # ── 装饰器方式(@cron_register 在下,@cron.job 在上) ──
118
+
119
+ # 无参数任务
120
+ @cron.job("0 */5 * * * *")
121
+ @cron_register('report')
122
+ def report_task():
123
+ print("生成报告")
124
+
125
+ # 有参数任务(必须在装饰器中传 args/kwargs)
126
+ @cron.job("0 30 9 * * * *", args=("admin@example.com",), kwargs={"report_type": "daily"})
127
+ @cron_register('send_report_email')
128
+ def send_report_email(to_address: str, report_type: str = "daily"):
129
+ print(f"发送{report_type}报表到 {to_address}")
130
+
131
+ # 有参数任务(装饰器中直接传参)
132
+ @cron.job("0 0 2 * * *", args=("backup_db",), kwargs={"compress": True})
133
+ @cron_register('backup')
134
+ def backup_database(db_name: str, compress: bool = False):
135
+ print(f"备份数据库 {db_name}, 压缩={compress}")
136
+
137
+ # 异步任务也支持
138
+ @cron.job("30 0 9 * * 1-5", trigger="cron")
139
+ @cron_register('async_work')
140
+ async def async_task():
141
+ print("异步任务也支持!")
142
+
143
+ # 间隔任务
144
+ @cron.job("@every 30s", trigger="interval")
145
+ @cron_register('heartbeat')
146
+ def heartbeat():
147
+ print("心跳")
148
+
149
+ # 日期任务(一次性)
150
+ @cron.job("2026-10-01 09:00:00", trigger="date")
151
+ @cron_register('national_day')
152
+ def national_day_task():
153
+ print("国庆节任务")
154
+
155
+ # ── 非装饰器写法(适合第三方函数或动态注册) ──
156
+
157
+ # 方式 1:add_cron_register + add_job
158
+ def send_sms(phone: str, message: str):
159
+ """发送短信(第三方函数)"""
160
+ print(f"发送短信到 {phone}: {message}")
161
+
162
+ add_cron_register('send_sms', send_sms)
163
+ cron.add_job(
164
+ 'send_sms',
165
+ "0 0 8 * * *",
166
+ trigger="cron",
167
+ job_id="morning_sms",
168
+ name="早安短信",
169
+ args=("13800138000", "早上好!"),
170
+ )
171
+
172
+ # 方式 2:直接 add_job(自动注册)
173
+ from nb_cron import cron_register
174
+
175
+ def cleanup_logs(days: int = 7):
176
+ """清理日志"""
177
+ print(f"清理{days}天前的日志")
178
+
179
+ cron_register('cleanup_logs', cleanup_logs)
180
+ cron.add_job(
181
+ cleanup_logs, # 传函数对象
182
+ "0 0 3 * * 0",
183
+ trigger="cron",
184
+ job_id="weekly_cleanup",
185
+ name="每周清理",
186
+ kwargs={"days": 30}, # 覆盖默认参数
187
+ )
188
+
189
+ # 方式 3:批量注册任务
190
+ def process_order(order_id: int):
191
+ """处理订单"""
192
+ print(f"处理订单 {order_id}")
193
+
194
+ add_cron_register('process_order', process_order)
195
+
196
+ # 动态添加多个不同参数的任务
197
+ cron.add_job('process_order', "@every 5m", job_id="process_order_batch1", args=(1001,))
198
+ cron.add_job('process_order', "@every 5m", job_id="process_order_batch2", args=(1002,))
199
+ cron.add_job('process_order', "@every 5m", job_id="process_order_batch3", args=(1003,))
200
+
201
+ # ── 启动 ──
202
+ cron.start()
203
+
204
+ # ── 管理 ──
205
+ cron.pause_job("daily_backup")
206
+ cron.resume_job("daily_backup")
207
+ cron.trigger_job("daily_backup")
208
+ cron.remove_job("daily_backup")
209
+ jobs = cron.get_jobs()
210
+
211
+ # ── Cron 翻译 ──
212
+ print(explain_cron("0 30 9 * * *", "zh")) # "每天 09:30:00 执行"
213
+ print(explain_cron("0 30 9 * * *", "en")) # "At 09:30:00, every day"
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 项目隔离(name 参数)
219
+
220
+ `NbCron` 的第一个参数 `name` 是**必传**的,用于隔离不同项目的数据:
221
+
222
+ - **Redis**: keys 格式为 `nb_cron:{name}:jobs`、`nb_cron:{name}:metrics`、`nb_cron:{name}:due`、`nb_cron:{name}:lock:*`
223
+ - **MongoDB**: collections 为 `nb_cron_{name}_jobs`、`nb_cron_{name}_metrics`、`nb_cron_{name}_locks`
224
+ - **SQLAlchemy**: 表名为 `nb_cron_{name}_jobs`、`nb_cron_{name}_metrics`、`nb_cron_{name}_locks`
225
+ - **Web UI**: 侧边栏标题显示 `name`,方便区分
226
+
227
+ ```python
228
+ # 同一个 Redis,不同项目互不干扰
229
+ cron_a = NbCron("billing_service", "redis://localhost:6379/0")
230
+ cron_b = NbCron("user_service", "redis://localhost:6379/0")
231
+
232
+ # cron_a 只看到 billing_service:jobs 下的任务
233
+ # cron_b 只看到 user_service:jobs 下的任务
234
+ ```
235
+
236
+ 不传 `name` 或传空字符串会直接报错:
237
+
238
+ ```python
239
+ NbCron("") # ValueError: NbCron name 不能为空
240
+ NbCron() # TypeError: missing required argument 'name'
241
+ ```
242
+
243
+ ---
244
+
245
+ ## 时区支持
246
+
247
+ nb_cron 默认使用**本地时区**。所有时间(`next_run_time`、日期表达式解析等)都基于调度器的时区。
248
+
249
+ ```python
250
+ from datetime import timedelta, timezone
251
+
252
+ # 默认:本地时区(推荐)
253
+ cron = NbCron("my_project")
254
+
255
+ # 显式指定时区
256
+ cron = NbCron("my_project", tz=timezone(timedelta(hours=8))) # 东八区
257
+ cron = NbCron("my_project", tz=timezone.utc) # UTC
258
+
259
+ # Python 3.9+ 可用 zoneinfo
260
+ from zoneinfo import ZoneInfo
261
+ cron = NbCron("my_project", tz=ZoneInfo("Asia/Shanghai"))
262
+ ```
263
+
264
+ `tz` 参数接受任何 `datetime.tzinfo` 对象。
265
+
266
+ ---
267
+
268
+ ## 三种触发器类型
269
+
270
+ nb_cron 支持三种触发器类型,通过 `trigger` 参数显式指定(也可以不传,自动推断):
271
+
272
+ | trigger 值 | 含义 | expression 示例 |
273
+ |---|---|---|
274
+ | `"cron"` | 6 字段 cron 表达式 | `"0 */5 * * * *"` |
275
+ | `"interval"` | 固定间隔重复执行 | `"@every 30s"`, `"5m"`, `"2h"` |
276
+ | `"date"` | 指定时间执行一次 | `"2026-10-01 09:00:00"`, `"2026年10月01日"` |
277
+
278
+ ### 自动推断 vs 显式指定
279
+
280
+ ```python
281
+ # 自动推断(不传 trigger,nb_cron 自动判断)
282
+ @cron.job("0 */5 * * * *") # → cron
283
+ @cron.job("@every 30s") # → interval
284
+ @cron.job("2026-10-01 09:00:00") # → date
285
+
286
+ # 显式指定(推荐,语义更清晰)
287
+ @cron.job("0 */5 * * * *", trigger="cron")
288
+ @cron.job("@every 30s", trigger="interval")
289
+ @cron.job("2026-10-01 09:00:00", trigger="date")
290
+
291
+ # trigger="interval" 时支持简写(不需要 @every 前缀)
292
+ @cron.job("30s", trigger="interval") # 等价于 @every 30s
293
+ @cron.job("5m", trigger="interval") # 等价于 @every 5m
294
+ @cron.job("2h", trigger="interval") # 等价于 @every 2h
295
+ ```
296
+
297
+ > **注意:** 以上所有 `@cron.job` 下面都需要 `@cron_register('名称')` 装饰器。
298
+
299
+ ---
300
+
301
+ ## 函数注册(强制)
302
+
303
+ nb_cron **强制要求**所有定时函数必须通过 `@cron_register` 注册一个**稳定名称**(`cron_func_name`)。
304
+ 这确保函数标识不依赖文件路径——重命名文件、移动函数不会影响调度。
305
+
306
+ ```python
307
+ from nb_cron import cron_register, add_cron_register
308
+
309
+ # 装饰器注册
310
+ @cron_register('daily_backup')
311
+ def backup_db():
312
+ print("备份")
313
+
314
+ backup_db.cron_func_name # → 'daily_backup' (IDE 可补全)
315
+
316
+ # 函数调用注册(适合第三方函数)
317
+ def send_email():
318
+ print("发邮件")
319
+ add_cron_register('send_email', send_email)
320
+
321
+ # 也支持 cron_register 两参数形式
322
+ cron_register('send_email', send_email)
323
+ ```
324
+
325
+ ### 与 `@cron.job` 配合
326
+
327
+ `@cron_register` 放下面(靠近函数),`@cron.job` 放上面:
328
+
329
+ ```python
330
+ @cron.job("0 0 2 * * *", trigger="cron") # 第二步:读 .cron_func_name,注册调度
331
+ @cron_register('daily_backup') # 第一步:设 .cron_func_name,注册函数
332
+ def backup_db():
333
+ print("备份")
334
+ ```
335
+
336
+ ### `add_job` 三种传参方式
337
+
338
+ ```python
339
+ # 1. 传函数对象(自动读 .cron_func_name)
340
+ cron.add_job(backup_db, "0 0 2 * * *", trigger="cron")
341
+
342
+ # 2. 传注册名字符串
343
+ cron.add_job('daily_backup', "0 0 2 * * *", trigger="cron")
344
+
345
+ # 3. 传 .cron_func_name(IDE 安全,等价于方式 2)
346
+ cron.add_job(backup_db.cron_func_name, "0 0 2 * * *", trigger="cron")
347
+
348
+ # 4. 带参数的任务(args 和 kwargs)
349
+ def send_email(to: str, subject: str, body: str = ""):
350
+ print(f"发送邮件到 {to}: {subject}")
351
+
352
+ cron_register('send_email', send_email)
353
+
354
+ # 在装饰器中传参
355
+ @cron.job("0 9 * * * *", args=("admin@example.com", "日报"), kwargs={"body": "这是日报内容"})
356
+ @cron_register('daily_report_email')
357
+ def daily_report_email(to: str, subject: str, body: str = ""):
358
+ print(f"发送日报到 {to}")
359
+
360
+ # 或在 add_job 中传参
361
+ cron.add_job(
362
+ 'send_email',
363
+ "0 9 * * * *",
364
+ trigger="cron",
365
+ job_id="morning_email",
366
+ args=("user@example.com", "晨报"),
367
+ kwargs={"body": "这是晨报内容"},
368
+ )
369
+
370
+ # 5. 批量添加同函数不同参数的任务
371
+ def process_batch(batch_id: int):
372
+ print(f"处理批次 {batch_id}")
373
+
374
+ cron_register('process_batch', process_batch)
375
+ cron.add_job('process_batch', "@every 10m", job_id="batch_1", args=(1,))
376
+ cron.add_job('process_batch', "@every 10m", job_id="batch_2", args=(2,))
377
+ cron.add_job('process_batch', "@every 10m", job_id="batch_3", args=(3,))
378
+ ```
379
+
380
+ ### 未注册直接报错
381
+
382
+ ```python
383
+ @cron.job("0 */5 * * * *")
384
+ def simple_task(): # ❌ ValueError: 函数 'simple_task' 未注册 cron_func_name
385
+ pass
386
+ ```
387
+
388
+ ---
389
+
390
+ ## 函数找不到的处理
391
+
392
+ 如果 Redis 中存储了某个 job 但对应的定时函数没有被导入或已被删除:
393
+
394
+ - **失败次数 +1**(计入 metrics)
395
+ - **job 状态变为 `error`**
396
+ - **前端显示红色"异常"标签**,不再显示"运行中"误导用户
397
+ - **日志打印 ERROR 级别信息**
398
+ - **继续调度**(下次触发时再次尝试,便于热修复后自动恢复)
399
+
400
+ ---
401
+
402
+ ## Web UI 管理后台(重点)
403
+
404
+ nb_cron 自带漂亮的管理后台,包含:
405
+ - **仪表盘**:任务总数、运行中/已暂停/异常卡片、24小时执行趋势图、成功率饼图
406
+ - **任务管理**:列表搜索/筛选、暂停/恢复/立即执行/删除操作、新建任务对话框
407
+ - **任务详情**:执行指标图表、最近10次执行记录、错误日志
408
+ - **Cron 工具**:Cron 表达式翻译器
409
+ - **中英文切换**
410
+
411
+ 支持 **FastAPI、Flask、Django** 三种框架一键启动。
412
+
413
+ ---
414
+
415
+ ### 方式一:FastAPI 启动(推荐)
416
+
417
+ ```bash
418
+ pip install nb_cron[redis,fastapi]
419
+ ```
420
+
421
+ 创建 `app.py`:
422
+
423
+ ```python
424
+ from nb_cron import NbCron, cron_register
425
+ from nb_cron.web import get_fastapi_app
426
+
427
+ cron = NbCron("my_project", "redis://localhost:6379/0")
428
+
429
+ # 无参数任务
430
+ @cron.job("*/10 * * * * *", trigger="cron", name="心跳检测")
431
+ @cron_register('heartbeat')
432
+ def heartbeat():
433
+ print("heartbeat OK")
434
+
435
+ # 有参数任务(必须在装饰器中传 args/kwargs)
436
+ @cron.job("0 */5 * * * *", trigger="cron", name="数据同步", args=(), kwargs={"source": "mysql", "target": "redis"})
437
+ @cron_register('sync_data')
438
+ def sync_data(source: str = "mysql", target: str = "redis"):
439
+ print(f"sync from {source} to {target}")
440
+
441
+ # 有参数任务(装饰器中传参)
442
+ @cron.job("0 30 2 * * *", trigger="cron", name="每日备份", args=("prod_db",), kwargs={"compress": True})
443
+ @cron_register('daily_backup')
444
+ def daily_backup(db_name: str, compress: bool = False):
445
+ print(f"backup {db_name}, compress={compress}")
446
+
447
+ # 非装饰器写法:第三方函数
448
+ def send_email(to: str, subject: str, body: str):
449
+ """发送邮件(第三方库函数)"""
450
+ print(f"邮件已发送到 {to}: {subject}")
451
+
452
+ cron_register('send_email', send_email)
453
+ cron.add_job(
454
+ 'send_email',
455
+ "0 0 9 * * 1-5",
456
+ trigger="cron",
457
+ job_id="morning_report_email",
458
+ name="晨报邮件",
459
+ args=("admin@example.com", "每日晨报", "这是晨报内容"),
460
+ )
461
+
462
+ # 批量添加同函数不同参数的任务
463
+ def process_queue(queue_name: str):
464
+ """处理队列"""
465
+ print(f"processing queue: {queue_name}")
466
+
467
+ cron_register('process_queue', process_queue)
468
+ cron.add_job('process_queue', "@every 1m", job_id="process_queue_1", args=("queue_1",))
469
+ cron.add_job('process_queue', "@every 1m", job_id="process_queue_2", args=("queue_2",))
470
+ cron.add_job('process_queue', "@every 1m", job_id="process_queue_3", args=("queue_3",))
471
+
472
+ app = get_fastapi_app(cron)
473
+
474
+ @app.on_event("startup")
475
+ def startup():
476
+ cron.start()
477
+
478
+ @app.on_event("shutdown")
479
+ def shutdown():
480
+ cron.stop()
481
+ ```
482
+
483
+ 启动:
484
+
485
+ ```bash
486
+ uvicorn app:app --host 0.0.0.0 --port 8000 --reload
487
+ ```
488
+
489
+ 打开浏览器访问:
490
+
491
+ | 地址 | 说明 |
492
+ |---|---|
493
+ | http://localhost:8000/nb_cron/ui/ | 管理后台 UI 页面 |
494
+ | http://localhost:8000/nb_cron/api/jobs | REST API - 任务列表 |
495
+ | http://localhost:8000/nb_cron/api/health | 健康检查(含时区信息) |
496
+ | http://localhost:8000/nb_cron/api/dashboard/stats | 仪表盘统计数据 |
497
+ | http://localhost:8000/nb_cron/api/cron/explain?expression=0+*/5+*+*+*+* | Cron 翻译 |
498
+ | http://localhost:8000/docs | FastAPI 自动生成的 Swagger 文档 |
499
+
500
+ ---
501
+
502
+ ### 方式二:Flask 启动
503
+
504
+ ```bash
505
+ pip install nb_cron[redis,flask]
506
+ ```
507
+
508
+ 创建 `app.py`:
509
+
510
+ ```python
511
+ from nb_cron import NbCron, cron_register
512
+ from nb_cron.web import get_flask_app
513
+
514
+ cron = NbCron("my_project", "redis://localhost:6379/0")
515
+
516
+ # 无参数任务
517
+ @cron.job("*/10 * * * * *", trigger="cron", name="心跳")
518
+ @cron_register('heartbeat')
519
+ def heartbeat():
520
+ print("heartbeat OK")
521
+
522
+ # 有参数任务(必须在装饰器中传 args/kwargs)
523
+ @cron.job("0 */5 * * * *", trigger="cron", name="同步", kwargs={"source": "mysql", "target": "redis"})
524
+ @cron_register('sync_data')
525
+ def sync_data(source: str = "mysql", target: str = "redis"):
526
+ print(f"sync from {source} to {target}")
527
+
528
+ # 非装饰器写法
529
+ def cleanup(days: int = 7):
530
+ """清理日志"""
531
+ print(f"cleanup logs older than {days} days")
532
+
533
+ cron_register('cleanup', cleanup)
534
+ cron.add_job(
535
+ 'cleanup',
536
+ "0 0 3 * * 0",
537
+ trigger="cron",
538
+ job_id="weekly_cleanup",
539
+ name="每周清理",
540
+ kwargs={"days": 30},
541
+ )
542
+
543
+ app = get_flask_app(cron)
544
+
545
+ if __name__ == "__main__":
546
+ cron.start()
547
+ app.run(host="0.0.0.0", port=5000, debug=False)
548
+ ```
549
+
550
+ 启动:
551
+
552
+ ```bash
553
+ # 开发模式
554
+ python app.py
555
+
556
+ # 生产模式(注意: 只用 1 个 worker,或用 Redis 存储自动防重复)
557
+ gunicorn app:app -w 1 -b 0.0.0.0:5000
558
+ ```
559
+
560
+ 访问 http://localhost:5000/nb_cron/ui/
561
+
562
+ ---
563
+
564
+ ### 方式三:Django 启动
565
+
566
+ ```bash
567
+ pip install nb_cron[redis,django]
568
+ ```
569
+
570
+ **Step 1** — 创建调度器配置文件 `your_project/cron_config.py`:
571
+
572
+ ```python
573
+ from nb_cron import NbCron, cron_register
574
+
575
+ cron = NbCron("my_project", "redis://localhost:6379/0")
576
+
577
+ # 无参数任务
578
+ @cron.job("*/10 * * * * *", trigger="cron", name="心跳")
579
+ @cron_register('heartbeat')
580
+ def heartbeat():
581
+ print("heartbeat OK")
582
+
583
+ # 有参数任务(必须在装饰器中传 args/kwargs)
584
+ @cron.job("0 */5 * * * *", trigger="cron", name="同步", kwargs={"source": "mysql", "target": "redis"})
585
+ @cron_register('sync_data')
586
+ def sync_data(source: str = "mysql", target: str = "redis"):
587
+ print(f"sync from {source} to {target}")
588
+
589
+ # 非装饰器写法:批量添加任务
590
+ def process_order(order_id: int):
591
+ """处理订单"""
592
+ print(f"processing order {order_id}")
593
+
594
+ cron_register('process_order', process_order)
595
+ cron.add_job('process_order', "@every 5m", job_id="process_order_1", args=(1001,))
596
+ cron.add_job('process_order', "@every 5m", job_id="process_order_2", args=(1002,))
597
+ cron.add_job('process_order', "@every 5m", job_id="process_order_3", args=(1003,))
598
+ ```
599
+
600
+ **Step 2** — 在 `urls.py` 中挂载路由:
601
+
602
+ ```python
603
+ from django.contrib import admin
604
+ from django.urls import path
605
+ from your_project.cron_config import cron
606
+ from nb_cron.web import get_django_urls
607
+
608
+ urlpatterns = [
609
+ path('admin/', admin.site.urls),
610
+ ] + get_django_urls(cron)
611
+ ```
612
+
613
+ **Step 3** — 在 `apps.py` 中启动调度器(防止 reload 重复启动):
614
+
615
+ ```python
616
+ import os
617
+ from django.apps import AppConfig
618
+
619
+ class YourAppConfig(AppConfig):
620
+ name = 'your_app'
621
+
622
+ def ready(self):
623
+ if os.environ.get('RUN_MAIN') == 'true':
624
+ from your_project.cron_config import cron
625
+ cron.start()
626
+ ```
627
+
628
+ 启动:
629
+
630
+ ```bash
631
+ python manage.py runserver 0.0.0.0:8000
632
+ ```
633
+
634
+ 访问 http://localhost:8000/nb_cron/ui/
635
+
636
+ ---
637
+
638
+ ## 前端 UI 构建说明
639
+
640
+ nb_cron 的管理后台前端源码位于 `nb_cron_ui/` 目录,使用以下技术栈:
641
+
642
+ - **Vue 3** — 响应式前端框架
643
+ - **Element Plus** — UI 组件库
644
+ - **ECharts** — 图表库
645
+ - **Pinia** — 状态管理
646
+ - **Vue I18n** — 中英文国际化
647
+ - **Vue Router** — 路由管理
648
+ - **Vite** — 构建工具
649
+
650
+ ### 构建前端(发布前必须执行)
651
+
652
+ ```bash
653
+ cd nb_cron_ui
654
+ npm install # 安装依赖
655
+ npm run build # 编译,输出到 nb_cron/web/static/
656
+ ```
657
+
658
+ 构建完成后,`nb_cron/web/static/` 目录下会生成 `index.html` 和 `assets/` 目录,
659
+ Python 后端会自动读取并在 `/nb_cron/ui/` 路径下提供服务。
660
+
661
+ ### 前端开发模式
662
+
663
+ 如果你要修改前端代码:
664
+
665
+ ```bash
666
+ # 终端 1:启动后端 API(以 FastAPI 为例)
667
+ uvicorn your_app:app --port 8000
668
+
669
+ # 终端 2:启动前端开发服务器(自动代理 API 到 8000 端口)
670
+ cd nb_cron_ui
671
+ npm run dev
672
+ ```
673
+
674
+ Vite 开发服务器会自动将 `/nb_cron/api/` 请求代理到 `http://localhost:8000`,
675
+ 实现前后端分离开发、热更新。
676
+
677
+ ### 前端目录结构
678
+
679
+ ```
680
+ nb_cron_ui/
681
+ ├── package.json # 依赖配置
682
+ ├── vite.config.js # Vite 配置(base路径、构建输出、API代理)
683
+ ├── index.html # 入口 HTML
684
+ ├── src/
685
+ │ ├── main.js # Vue 应用入口
686
+ │ ├── App.vue # 根组件
687
+ │ ├── router/index.js # 路由配置
688
+ │ ├── stores/app.js # Pinia 状态管理(侧边栏、标签页、语言)
689
+ │ ├── i18n/ # 国际化
690
+ │ │ ├── index.js # i18n 配置
691
+ │ │ ├── zh.js # 中文翻译
692
+ │ │ └── en.js # 英文翻译
693
+ │ ├── api/index.js # Axios API 封装
694
+ │ ├── views/
695
+ │ │ ├── Dashboard.vue # 仪表盘(统计卡片 + ECharts 图表)
696
+ │ │ ├── JobList.vue # 任务列表(搜索/操作/状态展示/新建任务)
697
+ │ │ ├── JobDetail.vue # 任务详情(指标 + 执行记录 + 错误日志)
698
+ │ │ └── CronTool.vue # Cron 表达式翻译工具
699
+ │ └── components/
700
+ │ ├── AppLayout.vue # 整体布局(侧边栏 + 顶栏 + 内容区)
701
+ │ ├── Sidebar.vue # 左侧导航栏
702
+ │ └── TabsBar.vue # 右侧多标签栏
703
+ ```
704
+
705
+ ---
706
+
707
+ ## Cron 表达式
708
+
709
+ nb_cron **强制 6 字段** cron 表达式,消除歧义:
710
+
711
+ ```
712
+ ┌──────────── 秒 (0-59)
713
+ │ ┌────────── 分 (0-59)
714
+ │ │ ┌──────── 时 (0-23)
715
+ │ │ │ ┌────── 日 (1-31)
716
+ │ │ │ │ ┌──── 月 (1-12)
717
+ │ │ │ │ │ ┌── 周 (0-6, 0=周日)
718
+ │ │ │ │ │ │
719
+ * * * * * *
720
+ ```
721
+
722
+ **传入 5 字段会直接报错**,避免用户写错。
723
+
724
+ ### 常用示例
725
+
726
+ | 表达式 | 含义 |
727
+ |---|---|
728
+ | `* * * * * *` | 每秒 |
729
+ | `0 * * * * *` | 每分钟整秒 |
730
+ | `*/10 * * * * *` | 每10秒 |
731
+ | `0 */5 * * * *` | 每5分钟 |
732
+ | `0 0 * * * *` | 每小时整点 |
733
+ | `0 30 9 * * *` | 每天 09:30:00 |
734
+ | `0 0 9 * * 1-5` | 工作日 09:00:00 |
735
+ | `0 0 0 1 * *` | 每月1号 00:00:00 |
736
+ | `0 0 2 * * 0` | 每周日 02:00:00 |
737
+
738
+ ### 间隔表达式
739
+
740
+ 除了 cron,也支持 `@every` 简写(trigger 自动推断为 `interval`):
741
+
742
+ ```python
743
+ @cron.job("@every 30s") # 每30秒(自动推断)
744
+ @cron.job("@every 5m", trigger="interval") # 每5分钟(显式指定)
745
+ @cron.job("2h", trigger="interval") # 每2小时(简写,显式指定时可省略 @every)
746
+ @cron.job("@every 1d") # 每天
747
+ @cron.job("@every 1w") # 每周
748
+ ```
749
+
750
+ ### 日期表达式(一次性任务)
751
+
752
+ 支持多种日期格式(trigger 自动推断为 `date`),日期按调度器的时区解析:
753
+
754
+ ```python
755
+ @cron.job("2026-10-01 09:00:00", trigger="date") # ISO 格式
756
+ @cron.job("2026-10-01", trigger="date") # 仅日期(当天 00:00:00)
757
+ @cron.job("2026/10/01 09:00:00", trigger="date") # 斜线分隔
758
+ @cron.job("2026年10月01日", trigger="date") # 中文日期
759
+ ```
760
+
761
+ ### Cron 翻译功能
762
+
763
+ ```python
764
+ from nb_cron import explain_cron
765
+
766
+ print(explain_cron("0 30 9 * * *", "zh")) # "每天09:30:00执行"
767
+ print(explain_cron("0 30 9 * * *", "en")) # "At 09:30:00, every day"
768
+ print(explain_cron("0 0 9 * * 1-5", "zh")) # "每周一至周五09:00:00执行"
769
+ print(explain_cron("*/5 * * * * *", "zh")) # "每5秒执行"
770
+ ```
771
+
772
+ REST API 也支持翻译:`GET /nb_cron/api/cron/explain?expression=0+*/5+*+*+*+*`
773
+
774
+ ---
775
+
776
+ ## 分布式部署
777
+
778
+ nb_cron 在 Redis/MongoDB/SQLAlchemy 存储模式下**自动支持分布式**:
779
+
780
+ ```python
781
+ cron = NbCron("my_project", "redis://localhost:6379/0")
782
+ ```
783
+
784
+ **原理:** 每次任务触发时,调度器先用 `SET NX PX` (Redis) 或 `findOneAndUpdate` (MongoDB) 获取分布式锁。锁的 key 包含任务 ID 和触发时间戳,确保同一次触发只有一个实例执行。
785
+
786
+ 不需要额外配置,不需要 leader election,开箱即用。
787
+
788
+ ---
789
+
790
+ ## REST API
791
+
792
+ 所有 API 前缀:`/nb_cron/api/`
793
+
794
+ | 方法 | 路径 | 说明 |
795
+ |---|---|---|
796
+ | GET | `/jobs` | 获取所有任务(含指标) |
797
+ | GET | `/jobs/{job_id}` | 获取单个任务详情 |
798
+ | POST | `/jobs` | 创建任务 |
799
+ | DELETE | `/jobs/{job_id}` | 删除任务 |
800
+ | POST | `/jobs/{job_id}/pause` | 暂停任务 |
801
+ | POST | `/jobs/{job_id}/resume` | 恢复任务 |
802
+ | POST | `/jobs/{job_id}/trigger` | 立即触发一次 |
803
+ | GET | `/jobs/{job_id}/metrics` | 获取任务指标 |
804
+ | GET | `/dashboard/stats` | 仪表盘统计(含 error_count) |
805
+ | GET | `/cron/explain?expression=...` | Cron 表达式翻译 |
806
+ | GET | `/functions` | 获取已注册函数列表 |
807
+ | GET | `/health` | 健康检查(含时区信息) |
808
+
809
+ ### 创建任务 API
810
+
811
+ `POST /nb_cron/api/jobs`
812
+
813
+ ```json
814
+ {
815
+ "func_ref": "daily_backup",
816
+ "expression": "0 0 2 * * *",
817
+ "trigger": "cron",
818
+ "job_id": "my_job",
819
+ "name": "我的任务",
820
+ "args": ["backup_db"],
821
+ "kwargs": {"compress": true},
822
+ "max_instances": 1
823
+ }
824
+ ```
825
+
826
+ - `func_ref`: 函数的 `cron_func_name`(通过 `@cron_register` 注册的稳定名称)
827
+ - `trigger`: 可选 `"cron"` / `"interval"` / `"date"`,不传则自动推断
828
+ - `args`: 位置参数列表,会按顺序传给函数
829
+ - `kwargs`: 关键字参数字典,会作为命名参数传给函数
830
+
831
+ ### 创建带参数的任务示例
832
+
833
+ ```json
834
+ // 发送邮件任务
835
+ POST /nb_cron/api/jobs
836
+ {
837
+ "func_ref": "send_email",
838
+ "expression": "0 9 * * * *",
839
+ "trigger": "cron",
840
+ "job_id": "morning_email",
841
+ "name": "晨报邮件",
842
+ "args": ["admin@example.com", "每日晨报"],
843
+ "kwargs": {"body": "这是晨报内容"}
844
+ }
845
+
846
+ // 批量处理任务(同函数不同参数)
847
+ POST /nb_cron/api/jobs
848
+ {
849
+ "func_ref": "process_queue",
850
+ "expression": "@every 1m",
851
+ "trigger": "interval",
852
+ "job_id": "process_queue_1",
853
+ "name": "处理队列 1",
854
+ "args": ["queue_1"]
855
+ }
856
+
857
+ POST /nb_cron/api/jobs
858
+ {
859
+ "func_ref": "process_queue",
860
+ "expression": "@every 1m",
861
+ "trigger": "interval",
862
+ "job_id": "process_queue_2",
863
+ "name": "处理队列 2",
864
+ "args": ["queue_2"]
865
+ }
866
+ ```
867
+
868
+ ### 获取已注册函数
869
+
870
+ `GET /nb_cron/api/functions`
871
+
872
+ 从存储后端(Redis/MongoDB/SQLAlchemy)读取所有已注册的函数名称列表,支持跨 Git 项目共享。
873
+
874
+ **跨项目工作原理:**
875
+ - 项目 A 中用 `@cron_register` 标记的函数,会在 `cron.start()` 时自动同步到 Redis
876
+ - 项目 B 的 Web UI 通过此 API 读取 Redis 中的函数列表
877
+ - 即使项目 A 没有运行,函数列表依然可用(持久化在 Redis 中)
878
+
879
+ **示例:**
880
+ ```bash
881
+ # 项目 A:只标记函数,不添加定时任务
882
+ @cron_register('send_email')
883
+ def send_email(to, subject):
884
+ ...
885
+
886
+ @cron_register('generate_report')
887
+ def generate_report(type):
888
+ ...
889
+
890
+ cron.start() # 函数名自动同步到 Redis
891
+
892
+ # 项目 B:Web UI 调用 API
893
+ GET /nb_cron/api/functions
894
+ # 返回:{"functions": ["send_email", "generate_report"]}
895
+
896
+ # Web UI 下拉框显示这些函数,用户可以选择并创建定时任务
897
+ ```
898
+
899
+ ---
900
+
901
+ ## 存储后端
902
+
903
+ | 后端 | URL 格式 | 分布式锁 | 适用场景 |
904
+ |---|---|---|---|
905
+ | Memory | `None`(默认) | 进程内锁 | 开发/单实例 |
906
+ | Redis | `redis://host:port/db` | SET NX PX | 生产/分布式(推荐) |
907
+ | MongoDB | `mongodb://host:port/db` | findOneAndUpdate | 生产/分布式 |
908
+ | SQLAlchemy | `sqlite:///path` / `mysql+pymysql://...` | INSERT conflict | 生产/分布式 |
909
+
910
+ ---
911
+
912
+ ## 任务指标
913
+
914
+ nb_cron 自动收集每个任务的执行指标(固定大小,不爆内存):
915
+
916
+ - `total_runs` - 总执行次数
917
+ - `success_count` / `fail_count` - 成功/失败次数
918
+ - `last_run_at` - 最后执行时间
919
+ - `last_error` - 最后一次错误信息(截断500字符)
920
+ - `avg_duration_ms` / `max_duration_ms` / `min_duration_ms` - 执行耗时统计
921
+ - `recent_results` - 最近10次执行结果(环形缓冲区)
922
+ - `hourly_stats` - 24小时逐时统计(固定24个槽位)
923
+
924
+ Redis 中单个任务的指标占用不超过 2KB。
925
+
926
+ ---
927
+
928
+ ## 任务状态
929
+
930
+ | 状态 | 含义 | 前端显示 |
931
+ |---|---|---|
932
+ | `active` | 正常运行中 | 绿色 `运行中` |
933
+ | `paused` | 已暂停 | 黄色 `已暂停` |
934
+ | `error` | 定时函数未找到 | 红色 `异常` |
935
+
936
+ 当函数未找到时(如函数被删除、模块未导入),job 自动标记为 `error` 状态并计入失败次数。修复函数后,下次触发会自动恢复。
937
+
938
+ ---
939
+
940
+ ## API 参考
941
+
942
+ ### `NbCron(name, store_url=None, max_workers=20, tick_seconds=1.0, misfire_grace_seconds=60, tz=None)`
943
+
944
+ 创建调度器实例。
945
+
946
+ - `name`: **必传**,调度器名称。用于隔离不同项目的 Redis keys / MongoDB collections / SQL 表。多个项目共享同一个 Redis 时互不干扰。UI 侧边栏会显示此名称
947
+ - `store_url`: 存储后端 URL,None 表示内存存储
948
+ - `max_workers`: 线程池大小
949
+ - `tick_seconds`: 调度循环间隔(秒)
950
+ - `misfire_grace_seconds`: misfire 容忍时间(超过此时间的错过任务会被跳过)
951
+ - `tz`: 时区,默认 `None` 使用本地时区。接受任何 `datetime.tzinfo` 对象
952
+
953
+ ### `@cron.job(expression, *, trigger=None, job_id=None, name=None, args=(), kwargs=None, max_instances=1)`
954
+
955
+ 装饰器,注册定时任务。同时支持同步函数和 async 函数。**函数必须先用 `@cron_register` 注册。**
956
+
957
+ - `expression`: 触发表达式(cron / 间隔 / 日期时间)
958
+ - `trigger`: 触发器类型,可选 `"cron"` / `"interval"` / `"date"`,不传则自动推断
959
+
960
+ ### `cron.add_job(func, expression, *, trigger=None, job_id=None, name=None, ...)`
961
+
962
+ 编程方式添加任务,返回 Job 对象。
963
+
964
+ - `func`: 函数对象 **或** `cron_func_name` 字符串(通过 `@cron_register` 注册的名称)
965
+ - `expression`: 触发表达式
966
+ - `trigger`: 触发器类型,可选 `"cron"` / `"interval"` / `"date"`,不传则自动推断
967
+
968
+ ### `@cron_register(cron_func_name)` / `add_cron_register(cron_func_name, func)` / `cron_register(cron_func_name, func)`
969
+
970
+ 给函数绑定路径无关的稳定名称。被装饰的函数会多出 `.cron_func_name` 属性。
971
+
972
+ ```python
973
+ from nb_cron import cron_register, add_cron_register
974
+ ```
975
+
976
+ ### `cron.start()` / `cron.stop(wait=True)`
977
+
978
+ - `start()` **不阻塞**,立即返回,后面的代码继续执行。
979
+ - 主线程跑完后进程**不会退出**,定时任务持续运行。
980
+ - `Ctrl+C` 优雅停止。
981
+ - 不需要任何 `sleep` / `join` / `input` / `block` 参数。
982
+
983
+ ### `@every` 间隔任务首次何时执行
984
+
985
+ `@every 5s` 等 **IntervalTrigger** 首次 `next_run` 为「注册时刻的 `now`」,会在**下一个调度 tick 内**执行第一次,**不会**先空等 5 秒;之后每隔 5 秒一次。
986
+
987
+ ### `cron.pause_job(job_id)` / `cron.resume_job(job_id)`
988
+
989
+ 暂停/恢复任务。
990
+
991
+ ### `cron.trigger_job(job_id)`
992
+
993
+ 立即执行一次(不等待下次触发时间)。
994
+
995
+ ### `cron.get_jobs()` / `cron.get_job(job_id)` / `cron.remove_job(job_id)`
996
+
997
+ 查询/删除任务。
998
+
999
+ ### `explain_cron(expression, lang="en")`
1000
+
1001
+ 翻译 cron 表达式为人类可读文本。`lang` 支持 `"zh"`(中文)和 `"en"`(英文)。
1002
+
1003
+ ### `get_fastapi_app(cron)` / `get_flask_app(cron)` / `get_django_urls(cron)`
1004
+
1005
+ 一行创建带 Web UI 的应用,分别返回 FastAPI app、Flask app、Django URL 列表。
1006
+
1007
+ ```python
1008
+ from nb_cron.web import get_fastapi_app, get_flask_app, get_django_urls
1009
+ ```
1010
+
1011
+ ---
1012
+
1013
+ ## 示例代码
1014
+
1015
+ 完整可运行的示例代码在 `examples/` 目录下:
1016
+
1017
+ | 文件 | 说明 |
1018
+ |---|---|
1019
+ | `example_fastapi_redis.py` | FastAPI + Redis 完整示例,含多个任务 |
1020
+ | `example_flask_redis.py` | Flask + Redis 完整示例 |
1021
+ | `example_django_redis.py` | Django + Redis 集成指南(分步说明) |
1022
+ | `example_memory_simple.py` | 最简示例,内存存储,无需 Redis |
1023
+ | `demo_cross_git_project_manage_corn_tasks/proj1.py` | **跨项目示例 - 项目 1**:函数定义与定时任务 |
1024
+ | `demo_cross_git_project_manage_corn_tasks/proj2_fastapi_cron.py` | **跨项目示例 - 项目 2**:Web UI 管理后台 |
1025
+
1026
+ ---
1027
+
1028
+ ## 跨 Git 项目示例(重点)
1029
+
1030
+ nb_cron 的核心特性:函数定义与任务调度分离,支持跨 Git 项目管理。
1031
+
1032
+ - **项目 1**(`proj1.py`):业务项目,用 `@cron_register` 标记函数,函数名自动同步到 Redis
1033
+ - **项目 2**(`proj2_fastapi_cron.py`):FastAPI 管理后台,通过 Web UI 为项目 1 的函数添加定时任务
1034
+
1035
+ ### 快速开始
1036
+
1037
+ ```bash
1038
+ # 终端 1:启动项目 1
1039
+ cd examples/demo_cross_git_project_manage_corn_tasks
1040
+ python proj1.py
1041
+
1042
+ # 终端 2:启动项目 2
1043
+ uvicorn proj2_fastapi_cron:app --reload
1044
+
1045
+ # 访问 Web UI:http://localhost:8000/nb_cron/ui/
1046
+ ```
1047
+
1048
+ ### 工作原理
1049
+
1050
+ ```
1051
+ 项目 1 (业务项目) Redis 项目 2 (管理后台)
1052
+ @cron_register('func') → 函数名列表 → GET /api/functions
1053
+ cron.start() 同步 → job 配置 ← POST /api/jobs 创建
1054
+ 执行函数 (本地进程) ← 调度任务 ← Web UI 操作
1055
+ ```
1056
+
1057
+ ### 优势
1058
+
1059
+ - **职责分离**:项目 1 专注业务,项目 2 专注调度
1060
+ - **安全**:只有 `@cron_register` 标记的函数才暴露
1061
+ - **灵活**:项目 2 动态创建任务,无需修改项目 1 的代码
1062
+ - **可维护**:项目 1 重构不影响项目 2 的调度
1063
+
1064
+ ### 应用场景
1065
+
1066
+ 微服务架构、多租户 SaaS、DevOps 自动化、数据平台等。
1067
+
1068
+ ---
1069
+
1070
+ ## Funboost 执行器(核弹级能力)
1071
+
1072
+ 关于funboost的教程请参考:[https://funboost.readthedocs.io/zh-cn/latest/index.html](https://funboost.readthedocs.io/zh-cn/latest/index.html)
1073
+
1074
+
1075
+ nb_cron 默认的执行器是在本地线程池中直接调用函数。但如果你将 `executor` 指定为 `FunboostExecutor`,nb_cron 的任务触发时**不在本地执行**,而是通过 funboost 的 `.push()` 把任务推送到消息队列(Redis / RabbitMQ / Kafka / MEMORY_QUEUE 等),由 funboost worker 进程消费执行。
1076
+
1077
+ 这意味着你**瞬间获得了 funboost 的全部能力**:
1078
+
1079
+ ### 为什么 FunboostExecutor 这么强?
1080
+
1081
+ 因为 funboost 的 `BoosterParams` 提供了**工业级**的任务消费能力,一个参数类就覆盖了 99% 的调度和函数运行控制需求:
1082
+
1083
+ | 能力 | BoosterParams 参数 | 说明 |
1084
+ |---|---|---|
1085
+ | **30+ 种消息队列** | `broker_kind` | Redis / RabbitMQ / Kafka / RocketMQ / Celery / SQS ... 30+ 种中间件随意切换 |
1086
+ | **精准控频** | `qps` | 指定每秒执行次数,支持小数(如 `0.01` = 每100秒1次),无需关心并发数 |
1087
+ | **分布式控频** | `is_using_distributed_frequency_control` | 多个消费者实例共享同一 qps 限额,总频率不超 |
1088
+ | **智能并发池** | `concurrent_num` + `concurrent_mode` | 线程/协程/协程+多进程/单线程,自适应扩缩容,任务少时自动缩减线程 |
1089
+ | **自动重试** | `max_retry_times` | 函数出错自动重试,支持指数退避(`is_using_advanced_retry`) |
1090
+ | **指数退避重试** | `advanced_retry_config` | `1s → 2s → 4s → 8s → 16s → 32s → 60s...`,支持 sleep 模式和 requeue 模式 |
1091
+ | **死信队列** | `is_push_to_dlx_queue_when_retry_max_times` | 重试耗尽后自动进入死信队列,不丢消息 |
1092
+ | **函数超时** | `function_timeout` | 运行超时自动杀死,防止任务卡死 |
1093
+ | **消息过期** | `msg_expire_seconds` | 消息超过指定时间自动丢弃,不执行过期任务 |
1094
+ | **任务去重** | `do_task_filtering` + `task_filtering_expire_seconds` | 相同参数的任务自动去重,防止重复执行 |
1095
+ | **运行时间限制** | `allow_run_time_cron` | 只在指定 cron 时间段内消费执行,如 `* 9-17 * * 1-5` 仅工作日上班时间 |
1096
+ | **执行结果持久化** | `function_result_status_persistance_conf` | 保存函数入参、运行结果、运行状态到 MongoDB,可追溯 |
1097
+ | **RPC 模式** | `is_using_rpc_mode` | 发布端可同步获取消费端的执行结果 |
1098
+ | **多进程消费** | `mp_consume(process_num=N)` | 协程/线程 + 多进程叠加,性能炸裂 |
1099
+ | **消费者分组** | `booster_group` | 按业务分组启动消费者,灵活管理 |
1100
+ | **自定义并发池** | `specify_concurrent_pool` | 多个消费者共享一个线程池,节约资源 |
1101
+ | **async 支持** | `specify_async_loop` | 指定 event loop,支持 aiohttp 等要求同 loop 的异步库 |
1102
+
1103
+ ### 安装
1104
+
1105
+ ```bash
1106
+ pip install nb_cron[redis] funboost
1107
+ ```
1108
+
1109
+ ### 基本用法
1110
+
1111
+ ```python
1112
+ from funboost import BoosterParams, BrokerEnum
1113
+ from nb_cron import NbCron, cron_register
1114
+ from nb_cron.executors.funboost_executor import FunboostExecutor
1115
+
1116
+ # 创建 funboost 执行器,BoosterParams 的所有参数都支持 IDE 自动补全
1117
+ funboost_executor = FunboostExecutor(
1118
+ BoosterParams(
1119
+ queue_name="nb_cron_dispatch", # 消息队列名
1120
+ broker_kind=BrokerEnum.REDIS, # 中间件类型,30+ 种可选
1121
+ concurrent_num=50, # 并发数
1122
+ qps=20, # 精准控频:每秒最多执行 20 次
1123
+ max_retry_times=3, # 失败自动重试 3 次
1124
+ is_using_distributed_frequency_control=True, # 分布式控频
1125
+ )
1126
+ )
1127
+
1128
+ # NbCron 构造时自动调用 executor.bind_cron(self)
1129
+ # worker 执行完后直接用 cron.metrics.record() 写指标,无需重建 store
1130
+ cron = NbCron("my_project", "redis://localhost:6379/0", executor=funboost_executor)
1131
+
1132
+ @cron.job("0 */5 * * * *", kwargs={"user_id": 42})
1133
+ @cron_register('my_task')
1134
+ def my_task(user_id: int):
1135
+ print(f"processing user {user_id}")
1136
+
1137
+ # 启动 funboost 消费者 + nb_cron 调度器
1138
+ funboost_executor.consume() # 单进程消费(非阻塞)
1139
+ # funboost_executor.mp_consume(process_num=4) # 多进程消费,性能炸裂
1140
+ cron.start()
1141
+ ```
1142
+
1143
+ ### 工作原理
1144
+
1145
+ ```
1146
+ nb_cron 调度器 funboost 消息队列 funboost worker
1147
+ cron tick 触发任务 ─push()→ Redis/RabbitMQ/Kafka ─consume()→ 解析 cron_func_name
1148
+ 计算 next_run_time 持久化存储,不丢消息 FunctionRegistry.resolve()
1149
+ 分布式锁防重复 执行函数 + 上报 metrics
1150
+ ```
1151
+
1152
+ **关键区别**:默认执行器是「调度 + 执行在同一进程」,FunboostExecutor 是「调度端 push,消费端执行」,天然解耦。
1153
+
1154
+ ### 高级用法
1155
+
1156
+ `BoosterParams` 的 入参非常丰富,各种函数控制功能都有,所以`nb_cron`的执行器可以充分借助`funboost`的威力,所以作者没有给`nb_cron`默认的本地线程池executor加太多功能,例如重试功能/超时杀死功能等。因为你即使没有安装任何消息队列,也可以 `BoosterParams(...,broker_kind=BrokerEnum.MEMORY_QUEUE)` 来使用funboost的内存队列。
1157
+
1158
+ #### 1. 指数退避重试
1159
+
1160
+ ```python
1161
+ funboost_executor = FunboostExecutor(
1162
+ BoosterParams(
1163
+ queue_name="nb_cron_dispatch",
1164
+ broker_kind=BrokerEnum.REDIS,
1165
+ max_retry_times=5,
1166
+ is_using_advanced_retry=True,
1167
+ advanced_retry_config={
1168
+ 'retry_mode': 'requeue', # requeue 模式:消息发回队列延迟重试,不占线程
1169
+ 'retry_base_interval': 1.0, # 基础间隔 1s
1170
+ 'retry_multiplier': 2.0, # 指数退避倍数
1171
+ 'retry_max_interval': 60.0, # 最大间隔 60s
1172
+ 'retry_jitter': True, # 随机抖动,防止惊群
1173
+ },
1174
+ )
1175
+ )
1176
+ # 重试间隔:1s → 2s → 4s → 8s → 16s → 32s → 60s → 60s...
1177
+ ```
1178
+ #### 2. Worker 独立进程部署(横向扩展)
1179
+
1180
+ 调度端和消费端可以完全分离部署,消费端可以独立横向扩展:
1181
+
1182
+ ```python
1183
+ # scheduler.py — 调度端(只 push,不消费)
1184
+ from nb_cron import NbCron, cron_register
1185
+ from nb_cron.executors.funboost_executor import FunboostExecutor
1186
+ from funboost import BoosterParams, BrokerEnum
1187
+
1188
+ funboost_executor = FunboostExecutor(
1189
+ BoosterParams(queue_name="nb_cron_dispatch", broker_kind=BrokerEnum.REDIS)
1190
+ )
1191
+ cron = NbCron("my_project", "redis://localhost:6379/0", executor=funboost_executor)
1192
+
1193
+ @cron.job("0 */5 * * * *")
1194
+ @cron_register('my_task')
1195
+ def my_task():
1196
+ print("执行任务")
1197
+
1198
+ cron.start() # 只调度,不消费
1199
+
1200
+ # worker.py — 消费端(独立进程,可部署多台机器)
1201
+ from nb_cron import NbCron
1202
+ from nb_cron.executors.funboost_executor import FunboostExecutor
1203
+ from funboost import BoosterParams, BrokerEnum
1204
+ import my_tasks # 触发 @cron_register,让注册表生效
1205
+
1206
+ funboost_executor = FunboostExecutor(
1207
+ BoosterParams(queue_name="nb_cron_dispatch", broker_kind=BrokerEnum.REDIS)
1208
+ )
1209
+ cron = NbCron("my_project", "redis://localhost:6379/0", executor=funboost_executor)
1210
+
1211
+ funboost_executor.mp_consume(process_num=4) # 4 进程消费
1212
+ ```
1213
+
1214
+ #### 3. 自定义指标回调
1215
+
1216
+ ```python
1217
+ def my_recorder(job_id, success, duration_ms, error):
1218
+ # 同时上报 Prometheus / 自定义监控系统
1219
+ prometheus_metrics.labels(job_id=job_id).observe(duration_ms)
1220
+
1221
+ funboost_executor = FunboostExecutor(
1222
+ BoosterParams(queue_name="nb_cron_dispatch", broker_kind=BrokerEnum.REDIS),
1223
+ metrics_recorder=my_recorder,
1224
+ )
1225
+ ```
1226
+
1227
+ ### FunboostExecutor vs 默认执行器
1228
+
1229
+ | 特性 | 默认 Executor | FunboostExecutor |
1230
+ |---|---|---|
1231
+ | 执行方式 | 本地线程池直接调用 | push 到消息队列,worker 消费执行 |
1232
+ | 消息持久化 | ❌ 进程崩溃任务丢失 | ✅ 消息队列持久化,不丢任务 |
1233
+ | 横向扩展 | ❌ 只能单进程 | ✅ worker 独立部署,无限扩展 |
1234
+ | 精准控频 | ❌ | ✅ qps 参数,支持分布式控频 |
1235
+ | 自动重试 | ❌ | ✅ max_retry_times + 指数退避 |
1236
+ | 任务去重 | ❌ | ✅ do_task_filtering |
1237
+ | 消息过期 | ❌ | ✅ msg_expire_seconds |
1238
+ | 死信队列 | ❌ | ✅ 重试耗尽自动进入死信队列 |
1239
+ | 函数超时 | ❌ | ✅ function_timeout |
1240
+ | 运行时间限制 | ❌ | ✅ allow_run_time_cron |
1241
+ | 30+ 种消息队列 | ❌ | ✅ broker_kind 一键切换 |
1242
+ | 多进程消费 | ❌ | ✅ mp_consume(process_num=N) |
1243
+ | 执行结果持久化 | ✅ (store) | ✅ (store + funboost MongoDB) |
1244
+
1245
+ ---
1246
+
1247
+ ## 运行示例
1248
+
1249
+ ```bash
1250
+ # FastAPI + Redis(推荐)
1251
+ cd examples
1252
+ pip install nb_cron[redis,fastapi]
1253
+ uvicorn example_fastapi_redis:app --reload
1254
+
1255
+ # Flask + Redis
1256
+ pip install nb_cron[redis,flask]
1257
+ python example_flask_redis.py
1258
+
1259
+ # 最简示例(无需 Redis)
1260
+ pip install nb_cron[fastapi]
1261
+ uvicorn example_memory_simple:app --reload
1262
+
1263
+ # 跨 Git 项目示例(重点推荐)
1264
+ # 终端 1:启动项目 1(业务项目)
1265
+ cd examples/demo_cross_git_project_manage_corn_tasks
1266
+ python proj1.py
1267
+
1268
+ # 终端 2:启动项目 2(FastAPI 管理后台)
1269
+ uvicorn proj2_fastapi_cron:app --reload
1270
+ ```
1271
+
1272
+ ---
1273
+
1274
+ ## License
1275
+
1276
+ MIT - ydf0509