202 lines
10 KiB
Python
202 lines
10 KiB
Python
# +======================================================================
|
||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||
# +----------------------------------------------------------------------
|
||
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
|
||
# +----------------------------------------------------------------------
|
||
# | 开源协议: Apache-2.0
|
||
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||
# +----------------------------------------------------------------------
|
||
# | 官方网站: https://www.djangoadmin.cn
|
||
# +----------------------------------------------------------------------
|
||
# | 研发团队: 小蚂蚁云团队 荣誉出品
|
||
# +======================================================================
|
||
# | 版权和免责声明:
|
||
# | 一、知识产权声明
|
||
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||
# | 二、使用限制与禁止条款
|
||
# | 您理解并同意,禁止将本产品用于以下任何行为:
|
||
# | 1. 任何违反中华人民共和国法律法规的用途;
|
||
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||
# | 三、责任豁免
|
||
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||
# | 四、违约与法律责任
|
||
# | 如发现任何违反本声明的行为,本团队有权:
|
||
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||
# | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||
# | 五、许可范围说明
|
||
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||
# | 成的一切经济损失及商誉损害。
|
||
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||
# | 许可协议》附件为准。
|
||
# +======================================================================
|
||
|
||
# +======================================================================
|
||
# | 模块: 定时任务闭环演示脚本
|
||
# | 说明: 打通「调度 → 执行 → 任务日志」完整闭环
|
||
# +======================================================================
|
||
|
||
"""
|
||
定时任务闭环演示脚本
|
||
=============================================================
|
||
|
||
打通「调度 → 执行 → 任务日志」完整闭环:
|
||
1. 向数据库新增一条测试任务(job_trigger=send_sms,job_executor 内置处理器,
|
||
仅打日志返回成功,无外部依赖)
|
||
2. 启动调度器(init_scheduler)按 cron 触发执行(execute_job)
|
||
3. 执行完成写入 fastapi_job_log(record_job_log),脚本打印新增日志证据
|
||
|
||
演示结束后测试任务置为未发布(status=0),避免后续调度器自动触发;
|
||
后台改回「运行中」并配置 cron 即可复现。
|
||
|
||
用法:
|
||
python scripts/demo_job_loop.py # cron 模式:自动等到下个整分钟边界触发一次
|
||
python scripts/demo_job_loop.py --wait 30 # cron 模式:手动等待 30s
|
||
python scripts/demo_job_loop.py --once # 立即执行一次(不走 cron 调度)
|
||
python scripts/demo_job_loop.py --cleanup # 演示后软删除测试任务及其日志
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
||
|
||
# Windows 控制台默认 GBK(cp936):把 stdout/stderr 切到 UTF-8,避免中文打印乱码/崩溃
|
||
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
sys.stderr.reconfigure(encoding='utf-8')
|
||
|
||
# ============================================================
|
||
# 测试任务配置
|
||
# ============================================================
|
||
TEST_JOB_NAME = "闭环测试-短信发送"
|
||
TEST_JOB_TRIGGER = "send_sms" # job_executor 内置处理器,无外部依赖
|
||
# 6 字段标准 cron:首字段为秒,每 15 秒触发一次(scheduler 已支持 5/6 字段)
|
||
TEST_CRON = "*/15 * * * * *"
|
||
|
||
|
||
# ============================================================
|
||
# 辅助函数
|
||
# ============================================================
|
||
def _insert_test_job(db):
|
||
"""新增测试任务(同名已存在则复用),返回 (job, 是否新建)"""
|
||
from modules.job.models import Job
|
||
job = db.query(Job).filter(Job.job_name == TEST_JOB_NAME, Job.is_delete == 0).first()
|
||
if job:
|
||
# 复用:确保置回运行中且 cron 正确,便于重复演示
|
||
db.query(Job).filter(Job.id == job.id).update(
|
||
{"status": 1, "cron_expression": TEST_CRON}, synchronize_session=False)
|
||
db.commit()
|
||
db.refresh(job)
|
||
return job, False
|
||
job = Job(
|
||
job_name=TEST_JOB_NAME,
|
||
job_alias="闭环演示",
|
||
job_group="演示",
|
||
job_trigger=TEST_JOB_TRIGGER,
|
||
status=1, # 运行中:调度器只加载 status==1 的任务
|
||
cron_expression=TEST_CRON,
|
||
execute_policy=1, # 立即执行
|
||
is_sync=0,
|
||
note="scripts/demo_job_loop.py 闭环演示用",
|
||
)
|
||
db.add(job)
|
||
db.commit()
|
||
return job, True
|
||
|
||
|
||
def _fetch_logs(job_id):
|
||
"""查询该任务最近的任务日志(独立新会话,避免主会话 REPEATABLE READ 快照看不到新日志)"""
|
||
from core.database import SessionLocal
|
||
from modules.job_log.models import JobLog
|
||
s = SessionLocal()
|
||
try:
|
||
return s.query(JobLog).filter(
|
||
JobLog.job_id == job_id, JobLog.is_delete == 0
|
||
).order_by(JobLog.id.desc()).limit(5).all()
|
||
finally:
|
||
s.close()
|
||
|
||
|
||
# ============================================================
|
||
# 主入口
|
||
# ============================================================
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="定时任务闭环演示:调度→执行→日志")
|
||
parser.add_argument("--once", action="store_true", help="立即执行一次(不依赖 cron 调度)")
|
||
parser.add_argument("--wait", type=int, default=0,
|
||
help="cron 模式轮询超时秒数;默认0=自动 25s(cron 每 15s 触发,25s 内必有新日志)")
|
||
parser.add_argument("--cleanup", action="store_true", help="演示后软删除测试任务及其日志")
|
||
args = parser.parse_args()
|
||
|
||
from core.database import SessionLocal
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
job, created = _insert_test_job(db)
|
||
print(f"测试任务: id={job.id} name={job.job_name} trigger={job.job_trigger} "
|
||
f"cron={job.cron_expression}({'新建' if created else '复用已存在'})")
|
||
|
||
before = _fetch_logs(job.id)
|
||
print(f"执行前该任务日志数: {len(before)}")
|
||
|
||
if args.once:
|
||
# 立即执行:execute_job → _record_execution_log → record_job_log(同步链路)
|
||
from modules.job.scheduler import execute_job
|
||
result = execute_job(job.id, job.url, job.is_sync, job.job_trigger, {})
|
||
print(f"execute_job 返回: {result}")
|
||
else:
|
||
# 调度器闭环:启动调度器加载 cron 任务 → 轮询等待新日志出现 → 关闭
|
||
from modules.job.scheduler import init_scheduler, shutdown_scheduler
|
||
loaded = init_scheduler()
|
||
timeout = args.wait if args.wait > 0 else 25
|
||
print(f"调度器已启动,加载 {loaded} 个启用任务(含测试任务),"
|
||
f"轮询等待新日志出现(最多 {timeout}s,cron 每 15s 触发)…")
|
||
deadline = time.time() + timeout
|
||
last_count = len(before)
|
||
while time.time() < deadline:
|
||
time.sleep(2)
|
||
if len(_fetch_logs(job.id)) > last_count:
|
||
break
|
||
shutdown_scheduler()
|
||
print("调度器已关闭")
|
||
|
||
after = _fetch_logs(job.id)
|
||
before_ids = {x.id for x in before}
|
||
new_logs = [log for log in after if log.id not in before_ids]
|
||
print(f"执行后日志数: {len(after)},本次新增 {len(new_logs)} 条")
|
||
for log in new_logs:
|
||
print(f" [job_log id={log.id}] status={log.status} message={log.job_message} "
|
||
f"consume={log.consume_time}ms exception={log.exception_info or ''}")
|
||
|
||
# 收尾:置为未发布,避免演示后调度器再次自动触发
|
||
db.query(job.__class__).filter(job.__class__.id == job.id).update(
|
||
{"status": 0}, synchronize_session=False)
|
||
db.commit()
|
||
print("演示结束,测试任务已置为未发布(status=0),后台改回运行中可复现")
|
||
|
||
if args.cleanup:
|
||
from modules.job_log.models import JobLog
|
||
db.query(JobLog).filter(JobLog.job_id == job.id, JobLog.is_delete == 0).update(
|
||
{"is_delete": 1}, synchronize_session=False)
|
||
db.query(job.__class__).filter(job.__class__.id == job.id).update(
|
||
{"is_delete": 1}, synchronize_session=False)
|
||
db.commit()
|
||
print("已软删除测试任务及其日志")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|