using System.Diagnostics;
using Medallion.Threading;

namespace Dpz.Core.Web.Library;

/// <summary>
/// 应用启动时自动将 wwwroot 目录下所有文件上传到对象存储
/// </summary>
internal sealed class AssetUploadHostedService(
    AssetService assetService,
    IFusionCache fusionCache,
    IDistributedLockProvider distributedLockProvider,
    ILogger<AssetUploadHostedService> logger
) : IHostedService
{
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LockKey = "Dpz.Core.AssetUpload.Startup";
    private const string LastSuccessCacheKey = "Dpz.Core.AssetUpload.LastSuccessUtc";
    private const int SkipMinutes = 30;
    private static readonly TimeSpan LockWaitTimeout = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan LastSuccessCacheTtl = TimeSpan.FromDays(7);

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await using var lockHandle = await TryAcquireStartupLockAsync(cancellationToken);
        if (lockHandle == null)
        {
            logger.LogInformation(
                "实例 {InstanceId} 未获取到资产上传锁,跳过本次上传",
                _instanceId
            );
            return;
        }

        var lastSuccess = await fusionCache.TryGetAsync<DateTimeOffset>(
            LastSuccessCacheKey,
            token: cancellationToken
        );
        if (lastSuccess.HasValue)
        {
            var elapsed = DateTimeOffset.UtcNow - lastSuccess.Value;
            if (elapsed < TimeSpan.FromMinutes(SkipMinutes))
            {
                logger.LogInformation(
                    "实例 {InstanceId} 跳过资产上传:距上次成功仅 {ElapsedMinutes:F1} 分钟,阈值 {SkipMinutes} 分钟",
                    _instanceId,
                    elapsed.TotalMinutes,
                    SkipMinutes
                );
                return;
            }
        }

        logger.LogInformation("实例 {InstanceId} 开始上传 wwwroot 资产到对象存储", _instanceId);
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await assetService.UploadAssentAsync(cancellationToken);

            stopwatch.Stop();

            await fusionCache.SetAsync(
                LastSuccessCacheKey,
                DateTimeOffset.UtcNow,
                LastSuccessCacheTtl,
                token: cancellationToken
            );

            logger.LogInformation(
                "实例 {InstanceId} 资产上传完成,耗时 {ElapsedMs}ms",
                _instanceId,
                stopwatch.ElapsedMilliseconds
            );
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            logger.LogError(
                ex,
                "实例 {InstanceId} 资产上传失败,耗时 {ElapsedMs}ms",
                _instanceId,
                stopwatch.ElapsedMilliseconds
            );
        }
    }

    private async Task<IDistributedSynchronizationHandle?> TryAcquireStartupLockAsync(
        CancellationToken cancellationToken
    )
    {
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        cts.CancelAfter(LockWaitTimeout);

        try
        {
            return await distributedLockProvider.AcquireLockAsync(
                LockKey,
                timeout: null,
                cancellationToken: cts.Token
            );
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            return null;
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个 ASP.NET Core 的后台服务(Hosted Service),主要功能是在应用启动时自动将 wwwroot 目录下的静态资源文件上传到对象存储服务。让我详细解释一下这个代码的各个部分:

类的基本结构

internal sealed class AssetUploadHostedService : IHostedService

这是一个实现了 IHostedService 接口的密封类,会在应用启动时自动运行。

依赖注入的服务

通过构造函数注入了以下服务:

  • AssetService:负责实际的资产上传逻辑
  • IFusionCache:分布式缓存,用于记录上次成功上传的时间
  • IDistributedLockProvider:分布式锁提供者,确保多实例环境下只有一个实例执行上传
  • ILogger:日志记录

核心常量和字段

private readonly string _instanceId = Guid.NewGuid().ToString();
private const string LockKey = "Dpz.Core.AssetUpload.Startup";
private const string LastSuccessCacheKey = "Dpz.Core.AssetUpload.LastSuccessUtc";
private const int SkipMinutes = 30;
  • _instanceId:每个实例的唯一标识
  • LockKey:分布式锁的键名
  • LastSuccessCacheKey:缓存上次成功时间的键名
  • SkipMinutes:如果距离上次成功上传不足30分钟,则跳过本次上传

主要逻辑 - StartAsync 方法

1. 获取分布式锁

await using var lockHandle = await TryAcquireStartupLockAsync(cancellationToken);
if (lockHandle == null) { /* 跳过上传 */ }

确保在多实例部署环境下,同一时间只有一个实例执行上传任务。

2. 检查上次成功时间

var lastSuccess = await fusionCache.TryGetAsync<DateTimeOffset>(LastSuccessCacheKey, token: cancellationToken);
if (lastSuccess.HasValue && elapsed < TimeSpan.FromMinutes(SkipMinutes)) { /* 跳过上传 */ }

如果距离上次成功上传不足30分钟,则跳过本次上传,避免频繁上传。

3. 执行上传

await assetService.UploadAssentAsync(cancellationToken);

调用 AssetService 执行实际的文件上传操作。

4. 记录成功状态

await fusionCache.SetAsync(LastSuccessCacheKey, DateTimeOffset.UtcNow, LastSuccessCacheTtl, token: cancellationToken);

上传成功后,将当前时间记录到缓存中,缓存有效期为7天。

分布式锁获取逻辑

private async Task<IDistributedSynchronizationHandle?> TryAcquireStartupLockAsync(CancellationToken cancellationToken)

这个方法尝试获取分布式锁,设置了5秒的超时时间。如果超时则返回 null,表示获取锁失败。

设计优势

  1. 防止重复执行:通过分布式锁确保多实例环境下不会重复上传
  2. 避免频繁上传:通过缓存记录上次成功时间,30分钟内不重复上传
  3. 性能监控:使用 Stopwatch 记录上传耗时
  4. 异常处理:完整的异常捕获和日志记录
  5. 资源管理:正确使用 await using 管理分布式锁资源

这个服务特别适用于微服务架构或容器化部署场景,能够智能地处理静态资源的上传同步问题。

评论加载中...