复查修复(三): 调度与扫描
- scan: .tmp/.part 过滤 + list_files 分批遍历(消除 10 万条截断误判) - Scheduler: 持锁只做标记, 锁外异步执行任务(长任务不再冻结调度) - cron 范围/步进边界校验;FTP 下载后大小校验防静默截断
This commit is contained in:
@@ -1308,6 +1308,12 @@ class MirrorSyncManager:
|
|||||||
else:
|
else:
|
||||||
ftp.retrbinary(f'RETR {remote_filename}', f.write)
|
ftp.retrbinary(f'RETR {remote_filename}', f.write)
|
||||||
|
|
||||||
|
# 校验下载大小与远端一致,防止静默截断
|
||||||
|
actual_size = os.path.getsize(local_path)
|
||||||
|
expected = file_info.get('size')
|
||||||
|
if expected is not None and expected > 0 and actual_size != expected:
|
||||||
|
raise Exception(f"文件大小不匹配: {actual_size} != {expected}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"下载FTP文件失败 {remote_filename}: {e}")
|
print(f"下载FTP文件失败 {remote_filename}: {e}")
|
||||||
|
|||||||
+23
-7
@@ -103,16 +103,27 @@ class ScheduledTask:
|
|||||||
result.extend(self._parse_cron_part(sub.strip(), min_val, max_val))
|
result.extend(self._parse_cron_part(sub.strip(), min_val, max_val))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 处理范围: "1-5"
|
# 处理范围: "1-5"(校验边界,越界返回空)
|
||||||
if '-' in part:
|
if '-' in part:
|
||||||
|
try:
|
||||||
start, end = part.split('-')
|
start, end = part.split('-')
|
||||||
return list(range(int(start), int(end) + 1))
|
start, end = int(start), int(end)
|
||||||
|
if not (min_val <= start <= end <= max_val):
|
||||||
|
return []
|
||||||
|
return list(range(start, end + 1))
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
|
||||||
# 处理步进: "*/5"
|
# 处理步进: "*/5"
|
||||||
if '/' in part:
|
if '/' in part:
|
||||||
base, step = part.split('/')
|
base, step = part.split('/')
|
||||||
base_list = self._parse_cron_part(base or '*', min_val, max_val)
|
base_list = self._parse_cron_part(base or '*', min_val, max_val)
|
||||||
|
try:
|
||||||
step = int(step)
|
step = int(step)
|
||||||
|
if step <= 0:
|
||||||
|
return []
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
return base_list[::step]
|
return base_list[::step]
|
||||||
|
|
||||||
# 单个值
|
# 单个值
|
||||||
@@ -301,18 +312,23 @@ class Scheduler:
|
|||||||
logger.info("定时任务调度器已停止")
|
logger.info("定时任务调度器已停止")
|
||||||
|
|
||||||
def _run_loop(self):
|
def _run_loop(self):
|
||||||
"""运行循环"""
|
"""运行循环(持锁只做标记,锁外异步执行任务,避免长任务冻结调度)"""
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
|
due = []
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for name, task in self.tasks.items():
|
for name, task in self.tasks.items():
|
||||||
if task.should_run_now():
|
if task.should_run_now():
|
||||||
# 使用线程池执行任务
|
due.append(task)
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
# 锁外异步执行,不阻塞后续任务的调度
|
||||||
executor.submit(task.run)
|
for task in due:
|
||||||
|
try:
|
||||||
|
threading.Thread(target=task.run, daemon=True).start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"调度任务 {task.name} 启动失败: {e}")
|
||||||
|
|
||||||
time.sleep(self.check_interval)
|
time.sleep(self.check_interval)
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -259,10 +259,12 @@ class SyncScheduler:
|
|||||||
self.last_scan_time = time.time()
|
self.last_scan_time = time.time()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 扫描文件
|
# 扫描文件(过滤传输中的临时文件)
|
||||||
scanned_files = []
|
scanned_files = []
|
||||||
for root, dirs, files in os.walk(base_dir):
|
for root, dirs, files in os.walk(base_dir):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
|
if filename.endswith(('.tmp', '.part')):
|
||||||
|
continue
|
||||||
full_path = os.path.join(root, filename)
|
full_path = os.path.join(root, filename)
|
||||||
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
|
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
|
||||||
|
|
||||||
@@ -276,9 +278,16 @@ class SyncScheduler:
|
|||||||
}
|
}
|
||||||
scanned_files.append(file_info)
|
scanned_files.append(file_info)
|
||||||
|
|
||||||
# 与数据库对比
|
# 与数据库对比(分批遍历,避免 limit 截断导致误判新增/删除)
|
||||||
db_files = self.db.list_files(limit=100000)
|
db_paths = set()
|
||||||
db_paths = {f.path for f in db_files if not f.is_dir}
|
offset = 0
|
||||||
|
BATCH = 5000
|
||||||
|
while True:
|
||||||
|
batch = self.db.list_files(limit=BATCH, offset=offset)
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
db_paths.update(f.path for f in batch if not f.is_dir)
|
||||||
|
offset += BATCH
|
||||||
|
|
||||||
# 检测新增
|
# 检测新增
|
||||||
local_paths = {f['path'] for f in scanned_files}
|
local_paths = {f['path'] for f in scanned_files}
|
||||||
|
|||||||
Reference in New Issue
Block a user