mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-16 22:35:32 +08:00
fix(dependencies): quote and validate Python source installs
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Node cron 调度器迁移
|
||||
|
||||
Node 调度路径使用固定版本 `node-cron@4.6.0`,普通任务(含额外规则)、订阅和系统 cron 共用 `back/shared/cronScheduler.ts`。启动/一次性任务、间隔任务、脚本并发限制和进程执行链路保持原有实现。系统模式只将语义一致的普通五字段规则交给 crond。
|
||||
|
||||
移除 `node-schedule`、其类型包以及兼容别名 `cron-parser-v4`。保留 `cron-parser@5` 做旧表达式的校验与标准化;计时器和日历推进由 node-cron 管理。
|
||||
|
||||
## 兼容行为
|
||||
|
||||
- 保留旧版 1–6 字段补全、五种宏、月份/星期名称、数字起点步长、L 和 #。另外明确新增 `@annually`(等同 `@yearly`)、`@midnight`(等同 `@daily`)、`@minutely`(每分钟第 0 秒)。宏按进程时区解释。`?` 仅允许日期和星期字段;裸 `/N`、H 和其他未列出的宏仍拒绝。
|
||||
- 日期与星期同时指定时保留 OR 语义,不采用 node-cron 默认的 AND。两个原生任务分别处理日期和星期,并按计划时间过滤交集,避免重复执行;保留旧解析器的月份全日期范围及全局 nth-week 限制。
|
||||
- 原生任务先暂停创建并验证,再替换旧任务。构造失败时销毁已准备的实例并保留旧快照。
|
||||
- 进程内迟到的触发通过 `execution:missed` 加入既有执行队列,记录补执行日志。并发和单任务积压上限仍由青龙队列控制;这不提供进程退出期间的持久化补跑或分布式 exactly-once 保证。
|
||||
- 标准化结果使用上限 512 条的只读缓存,仅缓存语法,不缓存运行实例或下次执行时间。避免大批相同表达式重复解析导致恢复 RPC 超时。
|
||||
- 回调异常记录日志,后续调度继续;取消时调用原生 `destroy()` 释放计时器和注册表。
|
||||
- 持久化无效任务在恢复时跳过并记录日志,无效订阅不会产生未处理的启动拒绝。API、导入和执行使用同一套表达式校验。
|
||||
|
||||
## 复审修复
|
||||
|
||||
- 移除没有生产调用的 `nextInvocation()` 接口,避免对 OR/# 等过滤规则报告不会实际执行的原生日期。回归测试直接观察实际执行和原生实例状态。
|
||||
- 批量注册 RPC 时限为 5 秒加每条规则 5 毫秒,计入额外规则并封顶 120 秒;保留超时后的恢复机制,不立即重放结果不确定的写入。
|
||||
- Alpine/系统模式下,数字起点步长(如 `0/5`)、星期包含 `7`、日期步长、日期和星期同时受限的表达式转入 Node 路径。BusyBox 的这些解析或通配语义与旧 Node 解析器不同,不能仅因是纯数字五字段就交给 crond。
|
||||
- 这些规则在 `crontab.list` 中仍保留为注释,避免同时被系统和 Node 执行。其他可移植规则仍使用系统 crond。
|
||||
|
||||
## 夏令时差异
|
||||
|
||||
采用 node-cron 的当地时间语义:春季跳时不存在的时间跳过,秋季重复小时不再执行第二次。旧 cron-parser 对部分春季时间会顺延,并可能重复秋季高频任务。使用有夏令时的时区时需要考虑该行为变化;Asia/Shanghai 不受影响。回归测试固定 America/New_York 的 2026 年转换日期。
|
||||
|
||||
## 验证
|
||||
|
||||
`npm test` 和 `npm run build:back`。
|
||||
|
||||
- `test/fixtures/legacy-cron.json` 是迁移前使用 node-schedule 2.1.1 / cron-parser 4.9.0 保存的快照:269 条语法判定,18 组日历序列,每组 10 次执行。
|
||||
- 真正的 node-cron 实例配合虚拟时钟验证日期/星期 OR、交集去重、闰年、L/#、8 秒迟到补执行、取消释放、回调拒绝和夏令时。
|
||||
- 注册构造失败回滚、无效订阅隔离、空表达式立即运行,以及现有调度恢复/503/子进程测试覆盖接入流程。
|
||||
- 隔离的正式 Debian 镜像使用实际编译产物验证 10,000 条有效任务和 2 条错误任务的恢复、300 个同时到期的 Python 脚本、并发上限 16 及 HTTP 健康检查。具体结果见本次测试报告;这是生产镜像隔离验证,不是线上长期运行证明。
|
||||
|
||||
### 本次验收结果(2026-09-27)
|
||||
|
||||
完整回归 260 通过、3 项原有跳过,后端编译成功。Node 20 生产运行时的 21 项兼容测试通过。隔离镜像中 300/300 脚本完成,无重复或遗漏,最大并发 16,270 个成功及 30 个预设失败均落库;完成耗时 52.36 秒。恢复后健康检查 1,870/1,870 成功,任务列表 38/38 成功。重启恢复阶段另有 50 次健康检查失败。
|
||||
|
||||
初版重复解析曾导致恢复超过 5 秒 RPC 时限,加入有上限的语法缓存后重跑通过。本次 10,000 个任务共享两个表达式,不能视为一万种复杂表达式的容量保证;实际脚本耗时也不代表纯调度性能。
|
||||
|
||||
新增宏在 UTC 和 Asia/Shanghai 下验证连续两次实际触发及不提前触发;原有语法快照仅对这三个新增宏允许预期变化。
|
||||
|
||||
### 复审后验收
|
||||
|
||||
完整回归 267 通过、3 项原有跳过;Node 20 的 10 项修复回归及 Alpine 镜像的 24 项兼容/宏测试通过。
|
||||
|
||||
额外使用 9,700 个不同表达式和 300 个同时到期任务验证恢复(另有 2 个无效任务),不再局限于共享两个表达式。300/300 脚本完成,无重复或遗漏,最大并发 16,270 个退出码 0、30 个预设退出码 3 均落库。恢复后健康检查 1,650/1,650 成功、任务列表 33/33 成功;启动恢复阶段另有 260 次健康检查失败,整批脚本耗时 60.27 秒。较大快照恢复仍需等待,不承诺启动阶段始终可用或任意规模都能在时限内恢复。
|
||||
@@ -0,0 +1,7 @@
|
||||
# Python 依赖来源
|
||||
|
||||
依赖管理中可手动添加 Python 包名、`git+https://` 等 pip 支持的源 URL,或脚本/订阅仓库目录内的 requirements 文件、pyproject.toml 路径。文件路径支持目录空格,必须位于持久化 scripts 或 repo 根目录内;越界与指向根目录外的符号链接会被拒绝。
|
||||
|
||||
requirements 使用 `pip install -r`,pyproject 使用完整项目目录安装,保留相对依赖文件和项目源码。拉取订阅不会自动安装仓库声明的依赖。
|
||||
|
||||
删除 requirements 条目会调用 `pip uninstall -r`。源 URL 或 pyproject 条目无法可靠逆向推断其安装的所有分发包,因此删除该条目只移除面板记录;需另外按包名卸载已安装的包,日志会明确提示。
|
||||
@@ -0,0 +1,78 @@
|
||||
# Task startup and notification loading
|
||||
|
||||
`task` performs shell configuration, status reporting and dependency-path discovery
|
||||
before invoking the selected runtime. These costs are separate from cron callback
|
||||
latency and time spent waiting for an execution slot.
|
||||
|
||||
## Dependency-path cache
|
||||
|
||||
Node scheduling usually starts in `/ql`, while Alpine crond starts in `/root`.
|
||||
The pnpm discovery key includes the working directory, npm/pnpm configuration,
|
||||
relevant environment and executable metadata. Each key now has its own cached
|
||||
record, so alternating scheduler environments do not evict each other.
|
||||
|
||||
Records expire after 60 seconds. Refreshes share a persistent per-user lock;
|
||||
records older than an hour are removed on refresh. Warm lookups do not scan the
|
||||
cache directory. The lock is never removed while another process might use it.
|
||||
The opt-out `QL_NODE_PATH_CACHE=0`, failed-discovery fallback, and package resolution
|
||||
order remain available. This caches the dependency directory, not its contents.
|
||||
|
||||
## Language startup paths
|
||||
|
||||
| Invocation | Environment and before hooks | Automatic notification loading |
|
||||
| --- | --- | --- |
|
||||
| `task script.py` / `.pyc` | Python preload imports generated environment, runs the shell/command before hooks, imports `task_before.py`, then applies account selection | `QLAPI.notify(...)` imports `__ql_notify__` on first call |
|
||||
| `task script.js` / `.mjs` / `.ts` | Node preload imports generated environment, runs shell/command before hooks, requires `task_before.js`, then applies account selection | `QLAPI.notify(...)` requires `__ql_notify__.js` on first call |
|
||||
| `task script.sh` | Shell sources the generated environment and runs shell before hooks | No automatic Python/Node notification module |
|
||||
| Explicit interpreter or other command, e.g. `task python3 script.py` | Retains generated environment and shell before-hook exports; it does not automatically install the Python/Node preload | No automatic notification module unless configured by the caller |
|
||||
|
||||
TypeScript uses `ts-node-transpile-only`; MJS uses the same Node preload plus the
|
||||
ESM loader. There are no separate built-in Ruby, Go, Java or PHP preload modules.
|
||||
All these task paths still pass through shared dependency-path setup: non-JS
|
||||
scripts and their hooks may themselves call Node or use the QLAPI bridge.
|
||||
|
||||
The before hooks are deliberately still synchronous: they can export environment
|
||||
variables or perform work required by the user's script. Python's preload still
|
||||
starts a child shell/Python to capture that environment; Node's preload starts a
|
||||
child shell/Node. Node also still loads its gRPC client eagerly. These are remaining
|
||||
startup costs, not work performed by the cron library, and are not removed by this
|
||||
change.
|
||||
|
||||
Shell scripts run in a child shell that inherits the wrapper's environment and
|
||||
before-hook functions. This lets the wrapper run its after hooks and report the
|
||||
exit status even when a script calls `exit`, replaces itself with `exec`, or fails
|
||||
under `set -e`. The script's own EXIT trap still runs in the child. Changes made
|
||||
inside the script (variables, working directory, functions and shell options)
|
||||
do not propagate back to the wrapper or its after hooks.
|
||||
|
||||
## Lazy notifications
|
||||
|
||||
Scripts keep calling `QLAPI.notify` with the same arguments. When no notification
|
||||
is sent, the notification provider and its channel dependencies are never imported
|
||||
by the preload. First use loads the provider; subsequent uses reuse the standard
|
||||
Python/Node module cache. JS forwards the receiver and return value, including
|
||||
promises, and Python forwards positional and keyword arguments. Import/send errors
|
||||
surface at the notification call rather than preventing unrelated scripts from
|
||||
obtaining QLAPI during startup. A failed import can be retried after its cause is
|
||||
resolved.
|
||||
|
||||
Provider initialization, including provider configuration reads, now happens at
|
||||
first use. A script's explicit `import __ql_notify__`, `require(...)`, or independent
|
||||
notification helper still loads that module immediately.
|
||||
|
||||
## Task timeouts
|
||||
|
||||
`task -m DURATION ...` also applies to sourced Shell scripts. The timeout worker
|
||||
inherits before-hook variables, arrays and functions without serializing them.
|
||||
At the deadline, the worker's process group receives TERM, followed by KILL after
|
||||
0.5 seconds, so ordinary child processes cannot continue after the timeout.
|
||||
Explicit interpreters and other commands use the same mechanism. Processes that
|
||||
intentionally create a separate session/process group are outside this boundary.
|
||||
The wrapper records status 124 and runs its after hook; concurrent account mode
|
||||
retains its existing aggregate status behavior. Zero disables the timeout.
|
||||
|
||||
Cron executions waiting for a concurrency slot retain their schedule revision.
|
||||
Disabling, deleting or replacing that revision prevents its queued executions
|
||||
from starting when a slot becomes available. Already running executions retain
|
||||
the existing stop behavior. Invalidated callbacks do not count against a new
|
||||
revision's limit of five pending/running executions.
|
||||
Reference in New Issue
Block a user