using System.Diagnostics;
using Dpz.Core.Service.Mediator.Features.Code.Commands;
using Hangfire;
using JetBrains.Annotations;
using LibGit2Sharp;
using Mediator;
namespace Dpz.Core.Web.Jobs.Hangfire;
/// <summary>
/// 同步代码任务执行器
/// </summary>
[UsedImplicitly]
public class SyncCodeActivator(
IMediator mediator,
IConfiguration configuration,
ILogger<SyncCodeActivator> logger
) : JobActivator
{
private static readonly SemaphoreSlim SyncLock = new(1, 1);
[ProlongExpirationTime]
public async Task SyncAsync()
{
// 检查是否有实例正在运行
if (!await SyncLock.WaitAsync(0))
{
logger.LogWarning("同步代码任务已有实例正在执行,跳过本次执行");
return;
}
try
{
var sourceCodePath = configuration["CodeView:SourceCodeRoot"];
if (string.IsNullOrWhiteSpace(sourceCodePath) || !Directory.Exists(sourceCodePath))
{
logger.LogError("源码目录:{Path},未配置或不存在", sourceCodePath);
return;
}
// Git 拉取
try
{
logger.LogInformation("正在拉取源代码");
var cancelTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30));
var st = new Stopwatch();
st.Start();
// 尝试清理可能存在的锁文件
CleanupGitLockFiles(sourceCodePath);
using (var repo = new Repository(sourceCodePath))
{
try
{
Commands.Checkout(repo, "master");
cancelTokenSource.Token.ThrowIfCancellationRequested();
}
catch (LockedFileException)
{
logger.LogWarning("检测到 Git 索引锁定,尝试清理锁文件后重试");
CleanupGitLockFiles(sourceCodePath);
cancelTokenSource.Token.ThrowIfCancellationRequested();
Commands.Checkout(repo, "master");
cancelTokenSource.Token.ThrowIfCancellationRequested();
}
var options = new PullOptions { FetchOptions = new FetchOptions() };
var signature = new Signature(
new Identity("dpz.core", "service@dpangzi.com"),
DateTimeOffset.Now
);
cancelTokenSource.Token.ThrowIfCancellationRequested();
var result = Commands.Pull(repo, signature, options);
if (result?.Status == MergeStatus.UpToDate)
{
logger.LogInformation("源代码已经是最新的");
}
}
st.Stop();
logger.LogInformation(
"源代码拉取完毕,耗时:{ElapsedMilliseconds}ms",
st.ElapsedMilliseconds
);
}
catch (Exception ex)
{
logger.LogError(ex, "拉取源代码失败");
return;
}
// 代码同步
try
{
logger.LogInformation("正在同步代码到数据库");
await mediator.Send(new SyncCodeFileSystemEntriesRequest());
}
catch (Exception ex)
{
logger.LogError(ex, "同步代码到数据库失败");
}
}
finally
{
SyncLock.Release();
}
}
/// <summary>
/// 清理 Git 锁文件
/// </summary>
/// <param name="repositoryPath">仓库路径</param>
private void CleanupGitLockFiles(string repositoryPath)
{
try
{
var gitDir = Path.Combine(repositoryPath, ".git");
var indexLockPath = Path.Combine(gitDir, "index.lock");
if (File.Exists(indexLockPath))
{
logger.LogWarning("发现残留的 index.lock 文件,正在删除:{Path}", indexLockPath);
File.Delete(indexLockPath);
logger.LogInformation("index.lock 文件已删除");
}
// 也检查 HEAD.lock
var headLockPath = Path.Combine(gitDir, "HEAD.lock");
if (File.Exists(headLockPath))
{
logger.LogWarning("发现残留的 HEAD.lock 文件,正在删除:{Path}", headLockPath);
File.Delete(headLockPath);
logger.LogInformation("HEAD.lock 文件已删除");
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "清理 Git 锁文件时发生异常");
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个基于 Hangfire 的后台任务执行器,用于定期同步 Git 仓库代码并将代码信息同步到数据库。
核心功能
1. 类定义与依赖注入
public class SyncCodeActivator(
IMediator mediator,
IConfiguration configuration,
ILogger<SyncCodeActivator> logger
) : JobActivator
- 继承自
JobActivator(Hangfire 的任务激活器基类) - 使用主构造函数注入三个依赖:
IMediator:用于发送命令(MediatR 模式)IConfiguration:读取配置信息ILogger:日志记录
2. 并发控制
private static readonly SemaphoreSlim SyncLock = new(1, 1);
- 使用信号量确保同一时刻只有一个任务实例运行
- 如果已有实例在执行,新任务会直接跳过:
if (!await SyncLock.WaitAsync(0))
{
logger.LogWarning("同步代码任务已有实例正在执行,跳过本次执行");
return;
}
3. 主同步流程 SyncAsync()
步骤 1:验证源码目录
var sourceCodePath = configuration["CodeView:SourceCodeRoot"];
if (string.IsNullOrWhiteSpace(sourceCodePath) || !Directory.Exists(sourceCodePath))
{
logger.LogError("源码目录:{Path},未配置或不存在", sourceCodePath);
return;
}
从配置中读取 CodeView:SourceCodeRoot,检查路径是否有效。
步骤 2:Git 拉取最新代码
using (var repo = new Repository(sourceCodePath))
{
Commands.Checkout(repo, "master");
var result = Commands.Pull(repo, signature, options);
}
关键处理:
超时控制:设置 30 分钟超时
var cancelTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30));锁文件处理:如果遇到
LockedFileException(Git 索引被锁定),会:- 调用
CleanupGitLockFiles()清理残留锁文件 - 重新尝试 checkout
- 调用
拉取结果判断:
if (result?.Status == MergeStatus.UpToDate) { logger.LogInformation("源代码已经是最新的"); }
步骤 3:同步到数据库
await mediator.Send(new SyncCodeFileSystemEntriesRequest());
通过 MediatR 发送命令,将文件系统中的代码信息同步到数据库。
4. 锁文件清理 CleanupGitLockFiles()
清理可能导致 Git 操作失败的残留锁文件:
var indexLockPath = Path.Combine(gitDir, "index.lock");
var headLockPath = Path.Combine(gitDir, "HEAD.lock");
这些锁文件通常在 Git 操作异常终止时产生,会阻止后续操作。
关键设计亮点
| 特性 | 实现方式 |
|---|---|
| 防并发 | SemaphoreSlim 确保单实例运行 |
| 健壮性 | 自动清理 Git 锁文件 + 重试机制 |
| 超时保护 | 30 分钟 CancellationToken |
| 性能监控 | 使用 Stopwatch 记录耗时 |
| 异常隔离 | Git 拉取失败不影响数据库同步的尝试 |
| Hangfire 特性 | [ProlongExpirationTime] 防止长时间任务被误删 |
典型使用场景
适用于需要定期从 Git 仓库同步代码并建立代码索引的系统,如:
- 代码搜索平台
- 代码审查系统
- 代码统计分析工具
- 在线代码浏览器
通过 Hangfire 配置定时任务(如每小时执行),自动保持代码库最新状态。
AI 正在分析代码…
评论加载中...