420 lines
14 KiB
Python
420 lines
14 KiB
Python
"""
|
||
任务二调度器模块
|
||
负责定时执行TAPD状态同步任务
|
||
|
||
功能:
|
||
1. 按配置频率定时执行
|
||
2. 优雅退出(Ctrl+C)
|
||
3. 运行统计
|
||
"""
|
||
|
||
import sys
|
||
import time
|
||
import signal
|
||
import argparse
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
# 将项目根目录添加到 Python 路径
|
||
project_root = Path(__file__).parent.parent
|
||
sys.path.insert(0, str(project_root))
|
||
|
||
# 导入schedule库
|
||
try:
|
||
import schedule
|
||
except ImportError:
|
||
print("错误: 缺少schedule库")
|
||
print("请运行: pip install schedule")
|
||
sys.exit(1)
|
||
|
||
from src.token_manager import TokenManager
|
||
from src2.config import Task2ConfigManager
|
||
from src2.sync_service import run_once
|
||
from src2.logger import get_task2_logger
|
||
|
||
|
||
class Task2Scheduler:
|
||
"""任务二调度器"""
|
||
|
||
def __init__(self, config_path=None, verbose=False):
|
||
"""
|
||
初始化调度器
|
||
|
||
Args:
|
||
config_path: 配置文件路径
|
||
verbose: 是否显示详细信息
|
||
"""
|
||
self.config_path = config_path
|
||
self.verbose = verbose
|
||
self.running = True
|
||
|
||
# 统计信息
|
||
self.stats = {
|
||
'start_time': None,
|
||
'total_runs': 0,
|
||
'success_runs': 0,
|
||
'failed_runs': 0,
|
||
'total_records_synced': 0,
|
||
'total_records_updated': 0,
|
||
'last_run_time': None
|
||
}
|
||
|
||
# 初始化配置管理器
|
||
self._init_config()
|
||
|
||
# 初始化TokenManager
|
||
self._init_token_manager()
|
||
|
||
# 获取调度配置
|
||
self.sync_interval = self.config['schedule']['sync_interval']
|
||
|
||
def _init_config(self):
|
||
"""初始化配置"""
|
||
try:
|
||
print("正在加载配置文件...")
|
||
self.config_manager = Task2ConfigManager(config_path=self.config_path)
|
||
self.config = self.config_manager.get_all_config()
|
||
print("✓ 配置文件加载成功")
|
||
except Exception as e:
|
||
print(f"✗ 配置文件加载失败: {e}")
|
||
raise
|
||
|
||
def _init_token_manager(self):
|
||
"""初始化TokenManager"""
|
||
try:
|
||
print("正在初始化TokenManager...")
|
||
self.token_manager = TokenManager(logger=get_task2_logger())
|
||
print("✓ TokenManager初始化成功")
|
||
except Exception as e:
|
||
print(f"✗ TokenManager初始化失败: {e}")
|
||
raise
|
||
|
||
def job(self):
|
||
"""执行一次同步任务"""
|
||
logger = get_task2_logger()
|
||
logger.start_sync(
|
||
trigger="task2_scheduler_job",
|
||
metadata={
|
||
"entry": "src2/scheduler.py:Task2Scheduler.job",
|
||
"verbose": self.verbose,
|
||
}
|
||
)
|
||
|
||
result = {
|
||
"success": False,
|
||
"docs_total": 0,
|
||
"docs_success": 0,
|
||
"docs_failed": 0,
|
||
"sheets_processed": 0,
|
||
"sheets_skipped": 0,
|
||
"total_records": 0,
|
||
"records_with_link": 0,
|
||
"records_synced": 0,
|
||
"records_updated": 0,
|
||
"records_failed": 0,
|
||
"error_message": None,
|
||
}
|
||
|
||
print("\n" + "=" * 80)
|
||
print(f"开始执行同步任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print("=" * 80)
|
||
|
||
try:
|
||
# 获取access_token
|
||
access_token = self.token_manager.get_token()
|
||
|
||
# 执行一次同步流程
|
||
result = run_once(
|
||
config_manager=self.config_manager,
|
||
access_token=access_token,
|
||
test_mode=self.verbose
|
||
)
|
||
|
||
# 更新统计信息
|
||
self.stats['total_runs'] += 1
|
||
self.stats['last_run_time'] = datetime.now()
|
||
|
||
if result['success']:
|
||
self.stats['success_runs'] += 1
|
||
self.stats['total_records_synced'] += result['records_synced']
|
||
self.stats['total_records_updated'] += result['records_updated']
|
||
|
||
print("\n" + "-" * 80)
|
||
print("本次执行统计:")
|
||
print(f" 处理子表: {result['sheets_processed']} 个")
|
||
print(f" 跳过子表: {result['sheets_skipped']} 个")
|
||
print(f" 包含链接: {result['records_with_link']} 条")
|
||
print(f" 同步成功: {result['records_synced']} 条")
|
||
print(f" 需要更新: {result['records_updated']} 条")
|
||
print(f" 同步失败: {result['records_failed']} 条")
|
||
print("-" * 80)
|
||
else:
|
||
self.stats['failed_runs'] += 1
|
||
print("\n" + "-" * 80)
|
||
print("本次执行失败:")
|
||
print(f" 错误信息: {result.get('error_message', '未知错误')}")
|
||
print("-" * 80)
|
||
|
||
logger.end_sync_with_stats(
|
||
stats={
|
||
"docs_total": result.get("docs_total", 0),
|
||
"docs_success": result.get("docs_success", 0),
|
||
"docs_failed": result.get("docs_failed", 0),
|
||
"sheets_processed": result.get("sheets_processed", 0),
|
||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||
"total_records": result.get("total_records", 0),
|
||
"records_with_link": result.get("records_with_link", 0),
|
||
"records_synced": result.get("records_synced", 0),
|
||
"records_updated": result.get("records_updated", 0),
|
||
"records_failed": result.get("records_failed", 0),
|
||
},
|
||
success=result.get("success", False),
|
||
error_message=result.get("error_message"),
|
||
extra={"source": "task2_scheduler_job"},
|
||
)
|
||
|
||
except Exception as e:
|
||
self.stats['total_runs'] += 1
|
||
self.stats['failed_runs'] += 1
|
||
self.stats['last_run_time'] = datetime.now()
|
||
|
||
print("\n" + "-" * 80)
|
||
print("本次执行异常:")
|
||
print(f" 错误类型: {type(e).__name__}")
|
||
print(f" 错误信息: {e}")
|
||
print("-" * 80)
|
||
|
||
if self.verbose:
|
||
import traceback
|
||
print("\n详细错误信息:")
|
||
traceback.print_exc()
|
||
|
||
if logger.get_active_sync_id():
|
||
logger.end_sync_with_stats(
|
||
stats={
|
||
"docs_total": result.get("docs_total", 0),
|
||
"docs_success": result.get("docs_success", 0),
|
||
"docs_failed": result.get("docs_failed", 0),
|
||
"sheets_processed": result.get("sheets_processed", 0),
|
||
"sheets_skipped": result.get("sheets_skipped", 0),
|
||
"total_records": result.get("total_records", 0),
|
||
"records_with_link": result.get("records_with_link", 0),
|
||
"records_synced": result.get("records_synced", 0),
|
||
"records_updated": result.get("records_updated", 0),
|
||
"records_failed": result.get("records_failed", 0),
|
||
},
|
||
success=False,
|
||
error_message=str(e),
|
||
extra={
|
||
"source": "task2_scheduler_job",
|
||
"exception_type": type(e).__name__,
|
||
},
|
||
)
|
||
|
||
# 显示下次执行时间
|
||
self._show_next_run_time()
|
||
|
||
def _show_next_run_time(self):
|
||
"""显示下次执行时间"""
|
||
next_run = schedule.idle_seconds()
|
||
if next_run is not None:
|
||
next_run_time = datetime.now().timestamp() + next_run
|
||
next_run_str = datetime.fromtimestamp(next_run_time).strftime('%Y-%m-%d %H:%M:%S')
|
||
print(f"\n下次执行时间: {next_run_str} (约 {int(next_run / 60)} 分钟后)")
|
||
|
||
def _startup_check(self):
|
||
"""启动检查"""
|
||
print("\n" + "=" * 80)
|
||
print("启动检查")
|
||
print("=" * 80)
|
||
|
||
checks_passed = True
|
||
|
||
# 1. 检查配置文件
|
||
print("\n[1/3] 检查配置文件...")
|
||
try:
|
||
print(f" ✓ 配置文件已加载: {self.config_path or '默认路径'}")
|
||
print(f" ✓ TAPD workspace_id: {self.config['tapd']['workspace_id']}")
|
||
print(f" ✓ SmartSheet docid: {self.config['smartsheet']['docid'][:20]}...")
|
||
print(f" ✓ 同步间隔: {self.sync_interval} 分钟")
|
||
except Exception as e:
|
||
print(f" ✗ 配置检查失败: {e}")
|
||
checks_passed = False
|
||
|
||
# 2. 检查环境变量
|
||
print("\n[2/3] 检查环境变量...")
|
||
import os
|
||
required_env_vars = {
|
||
'CORPID': '企业微信CorpID',
|
||
'CORPSECRET': '企业微信CorpSecret',
|
||
'TAPD_API_USER': 'TAPD API用户名',
|
||
'TAPD_API_PASSWORD': 'TAPD API密码'
|
||
}
|
||
|
||
for env_var, description in required_env_vars.items():
|
||
if os.environ.get(env_var):
|
||
print(f" ✓ {description} ({env_var}): 已设置")
|
||
else:
|
||
print(f" ✗ {description} ({env_var}): 未设置")
|
||
checks_passed = False
|
||
|
||
# 3. 测试access_token获取
|
||
print("\n[3/3] 测试access_token获取...")
|
||
try:
|
||
access_token = self.token_manager.get_token()
|
||
print(f" ✓ access_token获取成功: {access_token[:10]}...(已隐藏)")
|
||
except Exception as e:
|
||
print(f" ✗ access_token获取失败: {e}")
|
||
checks_passed = False
|
||
|
||
print("\n" + "=" * 80)
|
||
if checks_passed:
|
||
print("启动检查通过 ✓")
|
||
else:
|
||
print("启动检查失败 ✗")
|
||
print("\n请检查上述错误并修复后重试")
|
||
return False
|
||
print("=" * 80)
|
||
|
||
return True
|
||
|
||
def start(self):
|
||
"""启动调度器"""
|
||
print("\n" + "=" * 80)
|
||
print("TAPD状态同步调度器 (任务二)")
|
||
print("版本: 1.0.0 (第四阶段)")
|
||
print("=" * 80)
|
||
|
||
# 执行启动检查
|
||
if not self._startup_check():
|
||
print("\n调度器启动失败")
|
||
sys.exit(1)
|
||
|
||
# 记录启动时间
|
||
self.stats['start_time'] = datetime.now()
|
||
|
||
# 注册信号处理
|
||
signal.signal(signal.SIGINT, self._signal_handler)
|
||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||
|
||
# 配置定时任务
|
||
schedule.every(self.sync_interval).minutes.do(self.job)
|
||
|
||
print(f"\n调度器已启动:")
|
||
print(f" - 同步任务: 立即执行一次,然后每 {self.sync_interval} 分钟执行一次")
|
||
print("按 Ctrl+C 停止调度器")
|
||
print()
|
||
|
||
# 立即执行一次同步任务
|
||
self.job()
|
||
|
||
# 进入调度循环
|
||
while self.running:
|
||
schedule.run_pending()
|
||
time.sleep(1)
|
||
|
||
def _signal_handler(self, signum, frame):
|
||
"""信号处理函数"""
|
||
print("\n\n" + "=" * 80)
|
||
print("收到停止信号,正在优雅退出...")
|
||
print("=" * 80)
|
||
self.running = False
|
||
self._print_final_stats()
|
||
|
||
def _print_final_stats(self):
|
||
"""打印最终统计信息"""
|
||
print("\n" + "=" * 80)
|
||
print("运行统计")
|
||
print("=" * 80)
|
||
|
||
if self.stats['start_time']:
|
||
start_time_str = self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S')
|
||
print(f"启动时间: {start_time_str}")
|
||
|
||
if self.stats['last_run_time']:
|
||
last_run_str = self.stats['last_run_time'].strftime('%Y-%m-%d %H:%M:%S')
|
||
print(f"最后执行: {last_run_str}")
|
||
|
||
if self.stats['start_time']:
|
||
running_time = datetime.now() - self.stats['start_time']
|
||
hours = int(running_time.total_seconds() // 3600)
|
||
minutes = int((running_time.total_seconds() % 3600) // 60)
|
||
print(f"运行时长: {hours} 小时 {minutes} 分钟")
|
||
|
||
print(f"\n同步任务统计:")
|
||
print(f" 总执行次数: {self.stats['total_runs']}")
|
||
print(f" 成功次数: {self.stats['success_runs']}")
|
||
print(f" 失败次数: {self.stats['failed_runs']}")
|
||
print(f" 总同步记录: {self.stats['total_records_synced']}")
|
||
print(f" 总更新记录: {self.stats['total_records_updated']}")
|
||
|
||
if self.stats['total_runs'] > 0:
|
||
success_rate = (self.stats['success_runs'] / self.stats['total_runs']) * 100
|
||
print(f" 成功率: {success_rate:.1f}%")
|
||
|
||
print("=" * 80)
|
||
print("调度器已停止")
|
||
print("=" * 80)
|
||
|
||
|
||
def parse_arguments():
|
||
"""解析命令行参数"""
|
||
parser = argparse.ArgumentParser(
|
||
description='TAPD状态同步调度器 - 定时执行同步任务',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
示例用法:
|
||
# 使用默认配置启动调度器
|
||
python src2/scheduler.py
|
||
|
||
# 指定配置文件路径
|
||
python src2/scheduler.py --config /path/to/config.ini
|
||
|
||
# 显示详细信息
|
||
python src2/scheduler.py --verbose
|
||
"""
|
||
)
|
||
|
||
parser.add_argument(
|
||
'-c', '--config',
|
||
default=None,
|
||
help='配置文件路径(默认: config/config_task2.ini)'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'-v', '--verbose',
|
||
action='store_true',
|
||
help='显示详细输出信息'
|
||
)
|
||
|
||
return parser.parse_args()
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
try:
|
||
# 解析命令行参数
|
||
args = parse_arguments()
|
||
|
||
# 创建并启动调度器
|
||
scheduler = Task2Scheduler(
|
||
config_path=args.config,
|
||
verbose=args.verbose
|
||
)
|
||
scheduler.start()
|
||
|
||
return 0
|
||
|
||
except KeyboardInterrupt:
|
||
return 0
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 调度器启动失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|