From fffd0847a86df697780f8a6d3e087643b6ff5e2e Mon Sep 17 00:00:00 2001 From: sinclair Date: Sat, 15 Feb 2025 18:27:07 +0800 Subject: [PATCH 1/9] feat: app resource usage --- .../AppResources/Dto/AppResourceUsageDto.cs | 17 ++++++ .../Dto/GetAppResourceUsageInput.cs | 8 +++ .../AppResources/IAppResourceUsageService.cs | 12 +++++ .../AeFinderApplicationAutoMapperProfile.cs | 6 +++ .../AppResources/AppResourceUsageService.cs | 54 +++++++++++++++++++ .../ScheduledTask/AppResourceUsageWorker.cs | 30 +++++++++++ .../App/Es/AppResourceUsageIndex.cs | 25 +++++++++ .../AppResourceUsageServiceTests.cs | 46 ++++++++++++++++ 8 files changed, 198 insertions(+) create mode 100644 src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs create mode 100644 src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs create mode 100644 src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs create mode 100644 src/AeFinder.Application/AppResources/AppResourceUsageService.cs create mode 100644 src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs create mode 100644 src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs create mode 100644 test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs new file mode 100644 index 000000000..73e368627 --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using AeFinder.Apps; + +namespace AeFinder.AppResources.Dto; + +public class AppResourceUsageDto +{ + public AppInfoImmutable AppInfo { get; set; } + public Guid OrganizationId { get; set; } + public Dictionary ResourceUsages { get; set; } +} + +public class ResourceUsageDto +{ + public decimal StoreSize { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs new file mode 100644 index 000000000..1b2b803af --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/GetAppResourceUsageInput.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Application.Dtos; + +namespace AeFinder.AppResources.Dto; + +public class GetAppResourceUsageInput: PagedResultRequestDto +{ + public string AppId { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs new file mode 100644 index 000000000..9324cce8c --- /dev/null +++ b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using AeFinder.AppResources.Dto; +using Volo.Abp.Application.Dtos; + +namespace AeFinder.AppResources; + +public interface IAppResourceUsageService +{ + Task AddOrUpdateAsync(AppResourceUsageDto input); + Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input); +} \ No newline at end of file diff --git a/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs b/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs index 663c10f19..956a9b222 100644 --- a/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs +++ b/src/AeFinder.Application/AeFinderApplicationAutoMapperProfile.cs @@ -250,5 +250,11 @@ public AeFinderApplicationAutoMapperProfile() CreateMap(); CreateMap(); + + // AppResourceUsage + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs new file mode 100644 index 000000000..96d2e6e0f --- /dev/null +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AeFinder.App.Es; +using AeFinder.AppResources.Dto; +using AElf.EntityMapping.Repositories; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; +using System.Linq; + +namespace AeFinder.AppResources; + +[RemoteService(IsEnabled = false)] +[DisableAuditing] +public class AppResourceUsageService : AeFinderAppService, IAppResourceUsageService +{ + private readonly IEntityMappingRepository _entityMappingRepository; + + public AppResourceUsageService(IEntityMappingRepository entityMappingRepository) + { + _entityMappingRepository = entityMappingRepository; + } + + public async Task AddOrUpdateAsync(AppResourceUsageDto input) + { + var index = ObjectMapper.Map(input); + await _entityMappingRepository.AddOrUpdateAsync(index); + } + + public async Task> GetListAsync(Guid? organizationId, + GetAppResourceUsageInput input) + { + var queryable = await _entityMappingRepository.GetQueryableAsync(); + if (organizationId.HasValue) + { + queryable = queryable.Where(o => o.OrganizationId == organizationId); + } + + if (!input.AppId.IsNullOrWhiteSpace()) + { + queryable = queryable.Where(o => o.AppInfo.AppId == input.AppId); + } + + var count = queryable.Count(); + var list = queryable.OrderBy(o => o.AppInfo.AppId).Skip(input.SkipCount).Take(input.MaxResultCount).ToList(); + + return new PagedResultDto + { + TotalCount = count, + Items = ObjectMapper.Map, List>(list) + }; + } +} \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs new file mode 100644 index 000000000..f8a8d4574 --- /dev/null +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs @@ -0,0 +1,30 @@ +using AeFinder.AppResources; +using AElf.EntityMapping.Elasticsearch; +using Elasticsearch.Net; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace AeFinder.BackgroundWorker.ScheduledTask; + +public class AppResourceUsageWorker : AsyncPeriodicBackgroundWorkerBase, ISingletonDependency +{ + private readonly IAppResourceUsageService _appResourceUsageService; + private readonly IElasticsearchClientProvider _elasticsearchClientProvider; + + public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, + IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider) + : base(timer, serviceScopeFactory) + { + _appResourceUsageService = appResourceUsageService; + _elasticsearchClientProvider = elasticsearchClientProvider; + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + var client = _elasticsearchClientProvider.GetClient(); + var indices = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Kb)); + } + +} \ No newline at end of file diff --git a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs new file mode 100644 index 000000000..2a7e8708c --- /dev/null +++ b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using AeFinder.Entities; +using AElf.EntityMapping.Entities; +using Nest; + +namespace AeFinder.App.Es; + +public class AppResourceUsageIndex : AeFinderEntity, IEntityMappingEntity +{ + [Keyword] + public override string Id => AppInfo.AppId; + + public AppInfoImmutableIndex AppInfo { get; set; } + + [Keyword] + public Guid OrganizationId { get; set; } + + public Dictionary ResourceUsages { get; set; } +} + +public class ResourceUsageIndex +{ + public decimal StoreSize { get; set; } +} \ No newline at end of file diff --git a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs new file mode 100644 index 000000000..31451c9d0 --- /dev/null +++ b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Threading.Tasks; +using AeFinder.App.Es; +using AElf.EntityMapping.Elasticsearch; +using AElf.EntityMapping.Repositories; +using Elasticsearch.Net; +using Elasticsearch.Net.Specification.CatApi; +using Nest; +using Shouldly; +using Xunit; + +namespace AeFinder.AppResources; + +public class AppResourceUsageServiceTests : AeFinderApplicationAppTestBase +{ + private readonly IEntityMappingRepository _entityMappingRepository; + private readonly IElasticsearchClientProvider _elasticsearchClientProvider; + + public AppResourceUsageServiceTests() + { + _elasticsearchClientProvider = GetRequiredService(); + _entityMappingRepository = GetRequiredService>(); + } + + [Fact] + public async Task GetTest() + { + var client = _elasticsearchClientProvider.GetClient(); + + var index = client.LowLevel.Cat.Indices(new CatIndicesRequestParameters + { + Format = "json", + Headers = new string[] + { + "index", + "store.size", + "pri.store.size" + }, + Bytes = Bytes.Mb + }); + + var index2 = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Kb)); + ; + ; + } +} \ No newline at end of file From e210cb7b50fdd8ba60ecb53d7a031b9baf26c481 Mon Sep 17 00:00:00 2001 From: sinclair Date: Mon, 17 Feb 2025 19:29:29 +0800 Subject: [PATCH 2/9] feat: app resource usage worker --- .../AppResources/IAppResourceUsageService.cs | 1 + .../AppResources/AppResourceOptions.cs | 6 ++ .../AppResources/AppResourceUsageService.cs | 5 ++ .../Apps/AppDeployService.cs | 22 ++++- .../Options/ScheduledTaskOptions.cs | 1 + .../ScheduledTask/AppResourceUsageWorker.cs | 87 ++++++++++++++++++- .../ScheduledTask/CleanExpiredAssetWorker.cs | 29 ++++++- .../AeFinderBackGroundModule.cs | 1 + .../Controllers/AppResourceUsageController.cs | 47 ++++++++++ 9 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 src/AeFinder.Application/AppResources/AppResourceOptions.cs create mode 100644 src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs diff --git a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs index 9324cce8c..6317d6c1e 100644 --- a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs +++ b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs @@ -8,5 +8,6 @@ namespace AeFinder.AppResources; public interface IAppResourceUsageService { Task AddOrUpdateAsync(AppResourceUsageDto input); + Task DeleteAsync(string appId); Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input); } \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceOptions.cs b/src/AeFinder.Application/AppResources/AppResourceOptions.cs new file mode 100644 index 000000000..95eb9fe3c --- /dev/null +++ b/src/AeFinder.Application/AppResources/AppResourceOptions.cs @@ -0,0 +1,6 @@ +namespace AeFinder.AppResources; + +public class AppResourceOptions +{ + public int StoreReplicates { get; set; } = 5; +} \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs index 96d2e6e0f..a59306040 100644 --- a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -28,6 +28,11 @@ public async Task AddOrUpdateAsync(AppResourceUsageDto input) await _entityMappingRepository.AddOrUpdateAsync(index); } + public async Task DeleteAsync(string appId) + { + await _entityMappingRepository.DeleteAsync(appId); + } + public async Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input) { diff --git a/src/AeFinder.Application/Apps/AppDeployService.cs b/src/AeFinder.Application/Apps/AppDeployService.cs index 67c8db62d..5959eae9d 100644 --- a/src/AeFinder.Application/Apps/AppDeployService.cs +++ b/src/AeFinder.Application/Apps/AppDeployService.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using AeFinder.ApiKeys; using AeFinder.App.Deploy; +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; using AeFinder.Apps.Dto; using AeFinder.Assets; using AeFinder.BlockScan; @@ -40,6 +42,7 @@ public class AppDeployService : AeFinderAppService, IAppDeployService private readonly IAppEmailSender _appEmailSender; private readonly IApiKeyService _apiKeyService; private readonly PodResourceOptions _podResourceOptions; + private readonly IAppResourceUsageService _appResourceUsageService; public AppDeployService(IClusterClient clusterClient, IBlockScanAppService blockScanAppService, IAppDeployManager appDeployManager, @@ -50,12 +53,13 @@ public AppDeployService(IClusterClient clusterClient, IAssetService assetService, IApiKeyService apiKeyService, IUserAppService userAppService, IAppEmailSender appEmailSender, - IAppResourceLimitProvider appResourceLimitProvider) + IAppResourceLimitProvider appResourceLimitProvider, IAppResourceUsageService appResourceUsageService) { _clusterClient = clusterClient; _blockScanAppService = blockScanAppService; _appDeployManager = appDeployManager; _appResourceLimitProvider = appResourceLimitProvider; + _appResourceUsageService = appResourceUsageService; _organizationAppService = organizationAppService; _appDeployOptions = appDeployOptions.Value; _customOrganizationOptions = customOrganizationOptions.Value; @@ -388,6 +392,22 @@ await appResourceLimitGrain.SetAsync(new SetAppResourceLimitDto() throw new UserFriendlyException("Please purchase storage capacity before proceeding with deployment."); } } + else + { + var appResourceUsage = await _appResourceUsageService.GetListAsync(null, new GetAppResourceUsageInput + { + AppId = appId + }); + + if (appResourceUsage.Items.Any()) + { + var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(o => o.Value.StoreSize); + if (storageUsage > storageAsset.Replicas) + { + throw new UserFriendlyException("Storage is insufficient. Please purchase more storage."); + } + } + } } private async Task SetFirstDeployTimeAsync(string appId) diff --git a/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs b/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs index 8d63d99b7..95fde7b24 100644 --- a/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs +++ b/src/AeFinder.BackgroundWorker.Core/Options/ScheduledTaskOptions.cs @@ -21,4 +21,5 @@ public class ScheduledTaskOptions public int UnpaidBillTimeOutDays { get; set; } = 7; public int PayFailedOrderTimeoutHours { get; set; } = 48; public int ConfirmingOrderTimeoutMinutes { get; set; } = 60; + public int AppResourceUsageTaskPeriodMilliSeconds { get; set; } = 600000; } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs index f8a8d4574..a0b40558e 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs @@ -1,7 +1,13 @@ +using AeFinder.App.Es; using AeFinder.AppResources; +using AeFinder.AppResources.Dto; +using AeFinder.Apps; +using AeFinder.BackgroundWorker.Options; using AElf.EntityMapping.Elasticsearch; using Elasticsearch.Net; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Nest; using Volo.Abp.BackgroundWorkers; using Volo.Abp.DependencyInjection; using Volo.Abp.Threading; @@ -12,19 +18,94 @@ public class AppResourceUsageWorker : AsyncPeriodicBackgroundWorkerBase, ISingle { private readonly IAppResourceUsageService _appResourceUsageService; private readonly IElasticsearchClientProvider _elasticsearchClientProvider; + private readonly IAppService _appService; + private readonly AppResourceOptions _appResourceOptions; + private readonly ScheduledTaskOptions _scheduledTaskOptions; public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, - IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider) + IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider, + IAppService appService, IOptionsSnapshot appResourceOptions, + IOptionsSnapshot scheduledTaskOptions) : base(timer, serviceScopeFactory) { _appResourceUsageService = appResourceUsageService; _elasticsearchClientProvider = elasticsearchClientProvider; + _appService = appService; + _scheduledTaskOptions = scheduledTaskOptions.Value; + _appResourceOptions = appResourceOptions.Value; + + Timer.Period = _scheduledTaskOptions.AppResourceUsageTaskPeriodMilliSeconds; } protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { var client = _elasticsearchClientProvider.GetClient(); - var indices = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Kb)); + var catResponse = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Mb)); + + var indices = new Dictionary>(); + foreach (var record in catResponse.Records) + { + var appId = record.Index.Split('-')[0]; + if (!indices.TryGetValue(appId, out var records)) + { + records = new List(); + } + + records.Add(record); + indices[appId] = records; + } + + var skipCount = 0; + var maxResultCount = 100; + var apps = await _appService.GetIndexListAsync(new GetAppInput + { + SkipCount = skipCount, + MaxResultCount = maxResultCount + }); + + while (apps.Items.Count > 0) + { + foreach (var app in apps.Items) + { + if (!indices.TryGetValue(app.AppId, out var records)) + { + await _appResourceUsageService.DeleteAsync(app.AppId); + } + else + { + var appResourceUsage = new AppResourceUsageDto + { + AppInfo = new AppInfoImmutable + { + AppId = app.AppId, + AppName = app.AppName + }, + OrganizationId = Guid.Parse(app.OrganizationId), + ResourceUsages = new Dictionary() + }; + + foreach (var record in records) + { + var version = record.Index.Split('.')[0].Split('-')[1]; + if (!appResourceUsage.ResourceUsages.TryGetValue(version, out var resourceUsage)) + { + resourceUsage = new ResourceUsageDto(); + } + + resourceUsage.StoreSize += Convert.ToDecimal(record.PrimaryStoreSize) * + _appResourceOptions.StoreReplicates / 1024; + } + + await _appResourceUsageService.AddOrUpdateAsync(appResourceUsage); + } + } + + skipCount += maxResultCount; + apps = await _appService.GetIndexListAsync(new GetAppInput + { + SkipCount = skipCount, + MaxResultCount = maxResultCount + }); + } } - } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs index a8c15773f..03c67a637 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs @@ -1,3 +1,5 @@ +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; using AeFinder.Apps; using AeFinder.Assets; using AeFinder.BackgroundWorker.Options; @@ -31,6 +33,7 @@ public class CleanExpiredAssetWorker: AsyncPeriodicBackgroundWorkerBase, ISingle private readonly IUserAppService _userAppService; private readonly CustomOrganizationOptions _customOrganizationOptions; private readonly IBillingService _billingService; + private readonly IAppResourceUsageService _appResourceUsageService; public CleanExpiredAssetWorker(AbpAsyncTimer timer, ILogger logger, @@ -44,7 +47,7 @@ public CleanExpiredAssetWorker(AbpAsyncTimer timer, IUserAppService userAppService, IOptionsSnapshot customOrganizationOptions, IBillingService billingService, - IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, IAppResourceUsageService appResourceUsageService) : base(timer, serviceScopeFactory) { _logger = logger; _scheduledTaskOptions = scheduledTaskOptions.Value; @@ -57,6 +60,7 @@ public CleanExpiredAssetWorker(AbpAsyncTimer timer, _userAppService = userAppService; _customOrganizationOptions = customOrganizationOptions.Value; _billingService = billingService; + _appResourceUsageService = appResourceUsageService; // Timer.Period = 1 * 60 * 60 * 1000; // 3600000 milliseconds = 1 hours Timer.Period = _scheduledTaskOptions.CleanExpiredAssetTaskPeriodMilliSeconds; } @@ -130,6 +134,8 @@ private async Task ProcessAssetCleanAsync() await _appDeployService.DestroyAppAllSubscriptionAsync(appId); continue; } + + await CheckAppStorageAsync(appId, appAssets); } } @@ -216,4 +222,25 @@ private async Task> GetAeIndexerAssetListAsync(Guid organizationG return resultList; } + private async Task CheckAppStorageAsync(string appId, List appAssets) + { + var appResourceUsage = await _appResourceUsageService.GetListAsync(null, new GetAppResourceUsageInput + { + AppId = appId + }); + + if (appResourceUsage.Items.Any()) + { + var storageAsset = + appAssets.First(o => o.Status == AssetStatus.Using && o.Merchandise.Type == MerchandiseType.Storage); + var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(o => o.Value.StoreSize); + if (storageUsage > storageAsset.Replicas) + { + _logger.LogInformation( + "App {App}: storage uasge ({StorageUsage} GB) exceeds the purchased amount ({StorageAsset} GB).", + appId, storageUsage, storageAsset.Replicas); + await _appDeployService.DestroyAppAllSubscriptionAsync(appId); + } + } + } } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs b/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs index 745c1503b..f2ea50f1e 100644 --- a/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs +++ b/src/AeFinder.BackgroundWorker/AeFinderBackGroundModule.cs @@ -115,6 +115,7 @@ public override void OnApplicationInitialization(ApplicationInitializationContex AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); + AsyncHelper.RunSync(() => context.AddBackgroundWorkerAsync()); } public override void OnApplicationShutdown(ApplicationShutdownContext context) diff --git a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs new file mode 100644 index 000000000..83c1ba7a9 --- /dev/null +++ b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AeFinder.AppResources; +using AeFinder.AppResources.Dto; +using AeFinder.User; +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.Application.Dtos; + +namespace AeFinder.Controllers; + +[RemoteService] +[ControllerName("AppResource")] +[Route("api/apps/resource-usages")] +public class AppResourceUsageController : AeFinderController +{ + private readonly IAppResourceUsageService _appResourceUsageService; + private readonly IOrganizationAppService _organizationAppService; + + public AppResourceUsageController(IAppResourceUsageService appResourceUsageService, + IOrganizationAppService organizationAppService) + { + _appResourceUsageService = appResourceUsageService; + _organizationAppService = organizationAppService; + } + + [HttpGet] + public async Task> GetListAsync(GetAppResourceUsageInput input) + { + Guid? orgId = null; + if (!CurrentUser.IsInRole(AeFinderApplicationConsts.AdminRoleName)) + { + orgId = await GetOrganizationIdAsync(); + } + + return await _appResourceUsageService.GetListAsync(orgId, input); + } + + private async Task GetOrganizationIdAsync() + { + var organizationIds = await _organizationAppService.GetOrganizationUnitsByUserIdAsync(CurrentUser.Id.Value); + return organizationIds.First().Id; + } +} \ No newline at end of file From 83d28360f66523d6e2f97a413869579506b63083 Mon Sep 17 00:00:00 2001 From: sinclair Date: Tue, 18 Feb 2025 16:41:28 +0800 Subject: [PATCH 3/9] test: app resource usage --- .../AppResources/AppResourceUsageService.cs | 2 +- .../App/Es/AppResourceUsageIndex.cs | 2 +- .../AppResourceUsageServiceTests.cs | 77 +++++++++++++++---- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs index a59306040..7bc60a931 100644 --- a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -39,7 +39,7 @@ public async Task> GetListAsync(Guid? organi var queryable = await _entityMappingRepository.GetQueryableAsync(); if (organizationId.HasValue) { - queryable = queryable.Where(o => o.OrganizationId == organizationId); + queryable = queryable.Where(o => o.OrganizationId == organizationId.Value); } if (!input.AppId.IsNullOrWhiteSpace()) diff --git a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs index 2a7e8708c..08d2f9925 100644 --- a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs +++ b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs @@ -6,7 +6,7 @@ namespace AeFinder.App.Es; -public class AppResourceUsageIndex : AeFinderEntity, IEntityMappingEntity +public class AppResourceUsageIndex : AeFinderDomainEntity, IEntityMappingEntity { [Keyword] public override string Id => AppInfo.AppId; diff --git a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs index 31451c9d0..dc6ef2840 100644 --- a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs +++ b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AeFinder.App.Es; +using AeFinder.AppResources.Dto; +using AeFinder.Apps; using AElf.EntityMapping.Elasticsearch; using AElf.EntityMapping.Repositories; using Elasticsearch.Net; @@ -14,33 +17,77 @@ namespace AeFinder.AppResources; public class AppResourceUsageServiceTests : AeFinderApplicationAppTestBase { private readonly IEntityMappingRepository _entityMappingRepository; - private readonly IElasticsearchClientProvider _elasticsearchClientProvider; + private readonly IAppResourceUsageService _appResourceUsageService; public AppResourceUsageServiceTests() { - _elasticsearchClientProvider = GetRequiredService(); + _appResourceUsageService = GetRequiredService(); _entityMappingRepository = GetRequiredService>(); } [Fact] public async Task GetTest() { - var client = _elasticsearchClientProvider.GetClient(); - - var index = client.LowLevel.Cat.Indices(new CatIndicesRequestParameters + var appUsage1 = new AppResourceUsageDto + { + AppInfo = new AppInfoImmutable + { + AppId = "app1", + AppName = "app1" + }, + OrganizationId = AeFinderApplicationTestConsts.OrganizationId, + ResourceUsages = new Dictionary + { + { "version1", new ResourceUsageDto { StoreSize = 100 } }, + { "version2", new ResourceUsageDto { StoreSize = 200 } } + } + }; + await _appResourceUsageService.AddOrUpdateAsync(appUsage1); + + var appUsage2 = new AppResourceUsageDto { - Format = "json", - Headers = new string[] + AppInfo = new AppInfoImmutable { - "index", - "store.size", - "pri.store.size" + AppId = "app2", + AppName = "app2" }, - Bytes = Bytes.Mb - }); + OrganizationId = AeFinderApplicationTestConsts.OrganizationId, + ResourceUsages = new Dictionary + { + { "version1", new ResourceUsageDto { StoreSize = 110 } }, + { "version2", new ResourceUsageDto { StoreSize = 210 } } + } + }; + await _appResourceUsageService.AddOrUpdateAsync(appUsage2); + + var list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput()); + list.Items.Count.ShouldBe(2); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app1"); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app2"); + + list = await _appResourceUsageService.GetListAsync(null, + new GetAppResourceUsageInput()); + list.Items.Count.ShouldBe(2); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app1"); + list.Items.ShouldContain(o => o.AppInfo.AppId == "app2"); + + list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput + { + AppId = "app1" + }); + list.Items.Count.ShouldBe(1); + list.Items[0].AppInfo.AppId.ShouldBe("app1"); + list.Items[0].ResourceUsages.Sum(o => o.Value.StoreSize).ShouldBe(300); - var index2 = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Kb)); - ; - ; + await _appResourceUsageService.DeleteAsync("app1"); + + list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, + new GetAppResourceUsageInput + { + }); + list.Items.Count.ShouldBe(1); + list.Items[0].AppInfo.AppId.ShouldBe("app2"); } } \ No newline at end of file From 6d61f9339c85c1162c7495f0c40310a2fd074dc9 Mon Sep 17 00:00:00 2001 From: sinclair Date: Tue, 18 Feb 2025 17:51:21 +0800 Subject: [PATCH 4/9] fix: delete app --- .../EventHandler/AppDeleteHandler.cs | 12 +++++- src/AeFinder.Grains/Grain/Apps/AppGrain.cs | 6 +++ .../Grain/Apps/IOrganizationAppGrain.cs | 1 + .../Grain/Apps/OrganizationAppGrain.cs | 7 +++ .../Apps/AppGrainTests.cs | 43 +++++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs diff --git a/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs b/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs index 621613669..111a84548 100644 --- a/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs +++ b/src/AeFinder.BackgroundWorker.Core/EventHandler/AppDeleteHandler.cs @@ -24,20 +24,23 @@ public class AppDeleteHandler : IDistributedEventHandler, ITransie private readonly IBlockScanAppService _blockScanAppService; private readonly IEntityMappingRepository _appInfoEntityMappingRepository; private readonly IAssetService _assetService; + private readonly IEntityMappingRepository _organizationEntityMappingRepository; public AppDeleteHandler(ILogger logger, IClusterClient clusterClient, IBlockScanAppService blockScanAppService, IAssetService assetService, - IEntityMappingRepository appInfoEntityMappingRepository) + IEntityMappingRepository appInfoEntityMappingRepository, + IEntityMappingRepository organizationEntityMappingRepository) { _logger = logger; _clusterClient = clusterClient; _blockScanAppService = blockScanAppService; _assetService = assetService; _appInfoEntityMappingRepository = appInfoEntityMappingRepository; + _organizationEntityMappingRepository = organizationEntityMappingRepository; } - + public async Task HandleEventAsync(AppDeleteEto eventData) { var appId = eventData.AppId; @@ -82,6 +85,11 @@ public async Task HandleEventAsync(AppDeleteEto eventData) appInfoIndex.Status = eventData.Status; appInfoIndex.DeleteTime = eventData.DeleteTime; await _appInfoEntityMappingRepository.AddOrUpdateAsync(appInfoIndex); + + var organizationIndex = await _organizationEntityMappingRepository.GetAsync(organizationGuid.ToString()); + organizationIndex.AppIds.Remove(eventData.AppId); + await _organizationEntityMappingRepository.UpdateAsync(organizationIndex); + _logger.LogInformation($"[AppDeleteHandler] App {eventData.AppId} is deleted."); } diff --git a/src/AeFinder.Grains/Grain/Apps/AppGrain.cs b/src/AeFinder.Grains/Grain/Apps/AppGrain.cs index 93859c17e..0b04397e0 100644 --- a/src/AeFinder.Grains/Grain/Apps/AppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/AppGrain.cs @@ -119,6 +119,12 @@ public async Task UnFreezeAppAsync() public async Task DeleteAppAsync() { await ReadStateAsync(); + + var organizationAppGain = + GrainFactory.GetGrain( + GrainIdHelper.GenerateOrganizationAppGrainId(State.OrganizationId)); + await organizationAppGain.DeleteAppAsync(State.AppId); + State.Status = AppStatus.Deleted; State.DeleteTime = DateTime.UtcNow; await WriteStateAsync(); diff --git a/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs b/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs index fc04c76a6..4d5319f46 100644 --- a/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/IOrganizationAppGrain.cs @@ -6,6 +6,7 @@ public interface IOrganizationAppGrain : IGrainWithStringKey { Task AddOrganizationAsync(string organizationName); Task AddAppAsync(string appId); + Task DeleteAppAsync(string appId); Task> GetAppsAsync(); Task GetMaxAppCountAsync(); Task SetMaxAppCountAsync(int maxAppCount); diff --git a/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs b/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs index 17d1c2ce2..4311ab232 100644 --- a/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs +++ b/src/AeFinder.Grains/Grain/Apps/OrganizationAppGrain.cs @@ -48,6 +48,13 @@ public async Task AddAppAsync(string appId) await WriteStateAsync(); } + public async Task DeleteAppAsync(string appId) + { + await ReadStateAsync(); + State.AppIds.Remove(appId); + await WriteStateAsync(); + } + public async Task> GetAppsAsync() { await ReadStateAsync(); diff --git a/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs b/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs new file mode 100644 index 000000000..8f1e0b2b2 --- /dev/null +++ b/test/AeFinder.Grains.Tests/Apps/AppGrainTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using AeFinder.Apps; +using AeFinder.Grains.Grain.Apps; +using Shouldly; +using Xunit; + +namespace AeFinder.Grains.Apps; + +[Collection(ClusterCollection.Name)] +public class AppGrainTests : AeFinderGrainTestBase +{ + [Fact] + public async Task DeleteApp_Test() + { + var orgId = Guid.NewGuid(); + var createInput = new CreateAppDto + { + AppId = "appid", + OrganizationId = orgId.ToString("N"), + AppName = "app" + }; + + var appGrain = Cluster.Client.GetGrain(GrainIdHelper.GenerateAppGrainId(createInput.AppId)); + var app = await appGrain.CreateAsync(createInput); + app.AppId.ShouldBe(createInput.AppId); + + var organizationAppGain = + Cluster.Client.GetGrain( + GrainIdHelper.GenerateOrganizationAppGrainId(createInput.OrganizationId)); + var appIds = await organizationAppGain.GetAppsAsync(); + appIds.Count.ShouldBe(1); + appIds.ShouldContain(createInput.AppId); + + await appGrain.DeleteAppAsync(); + app = await appGrain.GetAsync(); + app.Status.ShouldBe(AppStatus.Deleted); + + appIds = await organizationAppGain.GetAppsAsync(); + appIds.Count.ShouldBe(0); + } +} \ No newline at end of file From 1de6338cb9b85cfb017edb30f10ab07f5f68130e Mon Sep 17 00:00:00 2001 From: sinclair Date: Tue, 18 Feb 2025 17:51:45 +0800 Subject: [PATCH 5/9] feat: remove request resource --- .../AppResources/Dto/AppFullPodResourceUsageDto.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs index 866483dc3..4d52ce9c5 100644 --- a/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppFullPodResourceUsageDto.cs @@ -4,10 +4,10 @@ public class AppFullPodResourceUsageDto { public string AppId { get; set; } public string AppVersion { get; set; } - public string ContainerName { get; set; } + // public string ContainerName { get; set; } public string CurrentState { get; set; } - public string RequestCpu { get; set; } - public string RequestMemory { get; set; } + // public string RequestCpu { get; set; } + // public string RequestMemory { get; set; } public string LimitCpu { get; set; } public string LimitMemory { get; set; } public long UsageTimestamp { get; set; } From e76d9afe114c5ef1aa178180cec54308bd075eee Mon Sep 17 00:00:00 2001 From: sinclair Date: Tue, 18 Feb 2025 18:05:00 +0800 Subject: [PATCH 6/9] fix: authorize --- src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs index 83c1ba7a9..7e6caf537 100644 --- a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs +++ b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs @@ -6,6 +6,7 @@ using AeFinder.AppResources.Dto; using AeFinder.User; using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Volo.Abp; using Volo.Abp.Application.Dtos; @@ -28,6 +29,7 @@ public AppResourceUsageController(IAppResourceUsageService appResourceUsageServi } [HttpGet] + [Authorize] public async Task> GetListAsync(GetAppResourceUsageInput input) { Guid? orgId = null; From 3ec63d196c17a70ccef81cd7de44d3b7a1e15bd3 Mon Sep 17 00:00:00 2001 From: sinclair Date: Tue, 18 Feb 2025 19:58:12 +0800 Subject: [PATCH 7/9] feat: app resource usage --- .../AeFinderApplicationConsts.cs | 1 + .../AppResources/Dto/AppResourceUsageDto.cs | 6 +- .../AppResources/IAppResourceUsageService.cs | 3 +- .../AppResources/AppResourceUsageService.cs | 6 +- .../Apps/AppDeployService.cs | 6 +- .../ScheduledTask/AppResourceUsageWorker.cs | 94 ++++++++++++++----- .../ScheduledTask/CleanExpiredAssetWorker.cs | 7 +- .../App/Es/AppResourceUsageIndex.cs | 7 +- .../AppResourceUsageServiceTests.cs | 69 ++++++++++++-- 9 files changed, 157 insertions(+), 42 deletions(-) diff --git a/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs b/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs index 9547286ff..5e53c0e96 100644 --- a/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs +++ b/src/AeFinder.Application.Contracts/AeFinderApplicationConsts.cs @@ -16,6 +16,7 @@ public class AeFinderApplicationConsts public const int DefaultAssetExpiration = 100; // 100 years public const string RegisterEmailTemplate = "Register"; public const string AdminRoleName = "admin"; + public const string AppStorageResourceName = "Storage"; public static readonly HashSet AppInterestedExtraPropertiesKey = new HashSet { "RefBlockNumber", "RefBlockPrefix", "ReturnValue", "Error", "TransactionFee", "ResourceFee" }; diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs index 73e368627..3db201213 100644 --- a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs @@ -8,10 +8,12 @@ public class AppResourceUsageDto { public AppInfoImmutable AppInfo { get; set; } public Guid OrganizationId { get; set; } - public Dictionary ResourceUsages { get; set; } + public Dictionary> ResourceUsages { get; set; } } public class ResourceUsageDto { - public decimal StoreSize { get; set; } + public string Name { get; set; } + public string Limit { get; set; } + public string Usage { get; set; } } \ No newline at end of file diff --git a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs index 6317d6c1e..cab931820 100644 --- a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs +++ b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using AeFinder.AppResources.Dto; using Volo.Abp.Application.Dtos; @@ -7,7 +8,7 @@ namespace AeFinder.AppResources; public interface IAppResourceUsageService { - Task AddOrUpdateAsync(AppResourceUsageDto input); + Task AddOrUpdateAsync(List input); Task DeleteAsync(string appId); Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input); } \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs index 7bc60a931..cca61ceb7 100644 --- a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -22,10 +22,10 @@ public AppResourceUsageService(IEntityMappingRepository input) { - var index = ObjectMapper.Map(input); - await _entityMappingRepository.AddOrUpdateAsync(index); + var index = ObjectMapper.Map, List>(input); + await _entityMappingRepository.AddOrUpdateManyAsync(index); } public async Task DeleteAsync(string appId) diff --git a/src/AeFinder.Application/Apps/AppDeployService.cs b/src/AeFinder.Application/Apps/AppDeployService.cs index 5959eae9d..195d40d46 100644 --- a/src/AeFinder.Application/Apps/AppDeployService.cs +++ b/src/AeFinder.Application/Apps/AppDeployService.cs @@ -401,7 +401,11 @@ await appResourceLimitGrain.SetAsync(new SetAppResourceLimitDto() if (appResourceUsage.Items.Any()) { - var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(o => o.Value.StoreSize); + var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(resourceUsages => + resourceUsages.Value + .Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName) + .Sum(resourceUsage => Convert.ToDecimal(resourceUsage.Usage))); + if (storageUsage > storageAsset.Replicas) { throw new UserFriendlyException("Storage is insufficient. Please purchase more storage."); diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs index a0b40558e..aba1a056e 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs @@ -2,7 +2,9 @@ using AeFinder.AppResources; using AeFinder.AppResources.Dto; using AeFinder.Apps; +using AeFinder.Assets; using AeFinder.BackgroundWorker.Options; +using AeFinder.Merchandises; using AElf.EntityMapping.Elasticsearch; using Elasticsearch.Net; using Microsoft.Extensions.DependencyInjection; @@ -21,6 +23,7 @@ public class AppResourceUsageWorker : AsyncPeriodicBackgroundWorkerBase, ISingle private readonly IAppService _appService; private readonly AppResourceOptions _appResourceOptions; private readonly ScheduledTaskOptions _scheduledTaskOptions; + private readonly IAssetService _assetService; public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider, @@ -33,28 +36,14 @@ public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceS _appService = appService; _scheduledTaskOptions = scheduledTaskOptions.Value; _appResourceOptions = appResourceOptions.Value; - + Timer.Period = _scheduledTaskOptions.AppResourceUsageTaskPeriodMilliSeconds; } protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { - var client = _elasticsearchClientProvider.GetClient(); - var catResponse = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Mb)); - - var indices = new Dictionary>(); - foreach (var record in catResponse.Records) - { - var appId = record.Index.Split('-')[0]; - if (!indices.TryGetValue(appId, out var records)) - { - records = new List(); - } - - records.Add(record); - indices[appId] = records; - } - + var indices = await GetIndicesRecordsAsync(); + var skipCount = 0; var maxResultCount = 100; var apps = await _appService.GetIndexListAsync(new GetAppInput @@ -65,6 +54,7 @@ protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext worker while (apps.Items.Count > 0) { + var toAddAppResourceUsage = new List(); foreach (var app in apps.Items) { if (!indices.TryGetValue(app.AppId, out var records)) @@ -81,22 +71,47 @@ protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext worker AppName = app.AppName }, OrganizationId = Guid.Parse(app.OrganizationId), - ResourceUsages = new Dictionary() + ResourceUsages = new Dictionary>() }; + var storeSizes = new Dictionary(); foreach (var record in records) { var version = record.Index.Split('.')[0].Split('-')[1]; - if (!appResourceUsage.ResourceUsages.TryGetValue(version, out var resourceUsage)) + if (!storeSizes.TryGetValue(version, out var usage)) { - resourceUsage = new ResourceUsageDto(); + usage = 0; } - resourceUsage.StoreSize += Convert.ToDecimal(record.PrimaryStoreSize) * - _appResourceOptions.StoreReplicates / 1024; + usage += Convert.ToDecimal(record.PrimaryStoreSize) * _appResourceOptions.StoreReplicates / + 1024; + storeSizes[version] = usage; + } + + var storageLimit = await GetStorageLimitAsync(Guid.Parse(app.OrganizationId), app.AppId); + + foreach (var storeSize in storeSizes) + { + if (!appResourceUsage.ResourceUsages.TryGetValue(storeSize.Key, out var resourceUsage)) + { + resourceUsage = new List(); + } + + resourceUsage.Add(new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = storageLimit.ToString(), + Usage = storeSize.Value.ToString("F2") + }); + appResourceUsage.ResourceUsages[storeSize.Key] = resourceUsage; } + + toAddAppResourceUsage.Add(appResourceUsage); + } - await _appResourceUsageService.AddOrUpdateAsync(appResourceUsage); + if (toAddAppResourceUsage.Count > 0) + { + await _appResourceUsageService.AddOrUpdateAsync(toAddAppResourceUsage); } } @@ -108,4 +123,37 @@ protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext worker }); } } + + private async Task>> GetIndicesRecordsAsync() + { + var client = _elasticsearchClientProvider.GetClient(); + var catResponse = await client.Cat.IndicesAsync(r => r.Bytes(Bytes.Mb)); + + var indices = new Dictionary>(); + foreach (var record in catResponse.Records) + { + var appId = record.Index.Split('-')[0]; + if (!indices.TryGetValue(appId, out var records)) + { + records = new List(); + } + + records.Add(record); + indices[appId] = records; + } + + return indices; + } + + private async Task GetStorageLimitAsync(Guid organizationId, string appId) + { + var storageAssets = await _assetService.GetListAsync(organizationId, new GetAssetInput() + { + Type = MerchandiseType.Storage, + AppId = appId, + IsDeploy = true + }); + var storageAsset = storageAssets.Items.FirstOrDefault(); + return storageAsset?.Replicas ?? 0; + } } \ No newline at end of file diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs index 03c67a637..a9105a34f 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs @@ -233,7 +233,12 @@ private async Task CheckAppStorageAsync(string appId, List appAssets) { var storageAsset = appAssets.First(o => o.Status == AssetStatus.Using && o.Merchandise.Type == MerchandiseType.Storage); - var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(o => o.Value.StoreSize); + + var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(resourceUsages => + resourceUsages.Value + .Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName) + .Sum(resourceUsage => Convert.ToDecimal(resourceUsage.Usage))); + if (storageUsage > storageAsset.Replicas) { _logger.LogInformation( diff --git a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs index 08d2f9925..9d1ae8ca7 100644 --- a/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs +++ b/src/AeFinder.Domain/App/Es/AppResourceUsageIndex.cs @@ -16,10 +16,13 @@ public class AppResourceUsageIndex : AeFinderDomainEntity, IEntityMappin [Keyword] public Guid OrganizationId { get; set; } - public Dictionary ResourceUsages { get; set; } + // Version -> ResourceUsage + public Dictionary> ResourceUsages { get; set; } } public class ResourceUsageIndex { - public decimal StoreSize { get; set; } + public string Name { get; set; } + public string Limit { get; set; } + public string Usage { get; set; } } \ No newline at end of file diff --git a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs index dc6ef2840..a758a1730 100644 --- a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs +++ b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -36,13 +37,32 @@ public async Task GetTest() AppName = "app1" }, OrganizationId = AeFinderApplicationTestConsts.OrganizationId, - ResourceUsages = new Dictionary + ResourceUsages = new Dictionary> { - { "version1", new ResourceUsageDto { StoreSize = 100 } }, - { "version2", new ResourceUsageDto { StoreSize = 200 } } + { + "version1", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "200", + Usage = "100" + } + } + }, + { + "version2", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "400", + Usage = "200" + } + } + } } }; - await _appResourceUsageService.AddOrUpdateAsync(appUsage1); var appUsage2 = new AppResourceUsageDto { @@ -52,13 +72,37 @@ public async Task GetTest() AppName = "app2" }, OrganizationId = AeFinderApplicationTestConsts.OrganizationId, - ResourceUsages = new Dictionary + ResourceUsages = new Dictionary> { - { "version1", new ResourceUsageDto { StoreSize = 110 } }, - { "version2", new ResourceUsageDto { StoreSize = 210 } } + { + "version1", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "2200", + Usage = "2100" + } + } + }, + { + "version2", new List + { + new ResourceUsageDto + { + Name = AeFinderApplicationConsts.AppStorageResourceName, + Limit = "600", + Usage = "300" + } + } + } } }; - await _appResourceUsageService.AddOrUpdateAsync(appUsage2); + await _appResourceUsageService.AddOrUpdateAsync(new List + { + appUsage1, + appUsage2 + }); var list = await _appResourceUsageService.GetListAsync(AeFinderApplicationTestConsts.OrganizationId, new GetAppResourceUsageInput()); @@ -79,7 +123,14 @@ public async Task GetTest() }); list.Items.Count.ShouldBe(1); list.Items[0].AppInfo.AppId.ShouldBe("app1"); - list.Items[0].ResourceUsages.Sum(o => o.Value.StoreSize).ShouldBe(300); + list.Items[0].ResourceUsages["version1"].Count().ShouldBe(1); + list.Items[0].ResourceUsages["version1"][0].Name.ShouldBe(AeFinderApplicationConsts.AppStorageResourceName); + list.Items[0].ResourceUsages["version1"][0].Limit.ShouldBe("200"); + list.Items[0].ResourceUsages["version1"][0].Usage.ShouldBe("100"); + list.Items[0].ResourceUsages["version2"].Count().ShouldBe(1); + list.Items[0].ResourceUsages["version2"][0].Name.ShouldBe(AeFinderApplicationConsts.AppStorageResourceName); + list.Items[0].ResourceUsages["version2"][0].Limit.ShouldBe("400"); + list.Items[0].ResourceUsages["version2"][0].Usage.ShouldBe("200"); await _appResourceUsageService.DeleteAsync("app1"); From f2e84a5193702a10a72d10fe45d5ef80d114f45c Mon Sep 17 00:00:00 2001 From: sinclair Date: Wed, 19 Feb 2025 11:03:16 +0800 Subject: [PATCH 8/9] feat: remove storage replicas --- .../AppResources/AppResourceOptions.cs | 6 ------ .../Apps/AppDeployService.cs | 14 ++++++------- .../ScheduledTask/AppResourceUsageWorker.cs | 10 ++++----- .../ScheduledTask/CleanExpiredAssetWorker.cs | 21 +++++++++++-------- 4 files changed, 23 insertions(+), 28 deletions(-) delete mode 100644 src/AeFinder.Application/AppResources/AppResourceOptions.cs diff --git a/src/AeFinder.Application/AppResources/AppResourceOptions.cs b/src/AeFinder.Application/AppResources/AppResourceOptions.cs deleted file mode 100644 index 95eb9fe3c..000000000 --- a/src/AeFinder.Application/AppResources/AppResourceOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AeFinder.AppResources; - -public class AppResourceOptions -{ - public int StoreReplicates { get; set; } = 5; -} \ No newline at end of file diff --git a/src/AeFinder.Application/Apps/AppDeployService.cs b/src/AeFinder.Application/Apps/AppDeployService.cs index 195d40d46..1127be43d 100644 --- a/src/AeFinder.Application/Apps/AppDeployService.cs +++ b/src/AeFinder.Application/Apps/AppDeployService.cs @@ -401,14 +401,14 @@ await appResourceLimitGrain.SetAsync(new SetAppResourceLimitDto() if (appResourceUsage.Items.Any()) { - var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(resourceUsages => - resourceUsages.Value - .Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName) - .Sum(resourceUsage => Convert.ToDecimal(resourceUsage.Usage))); - - if (storageUsage > storageAsset.Replicas) + foreach (var resourceUsage in appResourceUsage.Items.First().ResourceUsages.Values + .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => + resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) { - throw new UserFriendlyException("Storage is insufficient. Please purchase more storage."); + if (Convert.ToDecimal(resourceUsage.Usage) > storageAsset.Replicas) + { + throw new UserFriendlyException("Storage is insufficient. Please purchase more storage."); + } } } } diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs index aba1a056e..40a1ac3e0 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/AppResourceUsageWorker.cs @@ -21,21 +21,20 @@ public class AppResourceUsageWorker : AsyncPeriodicBackgroundWorkerBase, ISingle private readonly IAppResourceUsageService _appResourceUsageService; private readonly IElasticsearchClientProvider _elasticsearchClientProvider; private readonly IAppService _appService; - private readonly AppResourceOptions _appResourceOptions; private readonly ScheduledTaskOptions _scheduledTaskOptions; private readonly IAssetService _assetService; public AppResourceUsageWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, IAppResourceUsageService appResourceUsageService, IElasticsearchClientProvider elasticsearchClientProvider, - IAppService appService, IOptionsSnapshot appResourceOptions, - IOptionsSnapshot scheduledTaskOptions) + IAppService appService, + IOptionsSnapshot scheduledTaskOptions, IAssetService assetService) : base(timer, serviceScopeFactory) { _appResourceUsageService = appResourceUsageService; _elasticsearchClientProvider = elasticsearchClientProvider; _appService = appService; + _assetService = assetService; _scheduledTaskOptions = scheduledTaskOptions.Value; - _appResourceOptions = appResourceOptions.Value; Timer.Period = _scheduledTaskOptions.AppResourceUsageTaskPeriodMilliSeconds; } @@ -83,8 +82,7 @@ protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext worker usage = 0; } - usage += Convert.ToDecimal(record.PrimaryStoreSize) * _appResourceOptions.StoreReplicates / - 1024; + usage += Convert.ToDecimal(record.PrimaryStoreSize) / 1024; storeSizes[version] = usage; } diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs index a9105a34f..384c73d5c 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs @@ -233,18 +233,21 @@ private async Task CheckAppStorageAsync(string appId, List appAssets) { var storageAsset = appAssets.First(o => o.Status == AssetStatus.Using && o.Merchandise.Type == MerchandiseType.Storage); - - var storageUsage = appResourceUsage.Items.First().ResourceUsages.Sum(resourceUsages => - resourceUsages.Value - .Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName) - .Sum(resourceUsage => Convert.ToDecimal(resourceUsage.Usage))); - - if (storageUsage > storageAsset.Replicas) + + foreach (var resourceUsage in appResourceUsage.Items.First().ResourceUsages.Values + .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => + resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) { + if (Convert.ToDecimal(resourceUsage.Usage) <= storageAsset.Replicas) + { + continue; + } + _logger.LogInformation( - "App {App}: storage uasge ({StorageUsage} GB) exceeds the purchased amount ({StorageAsset} GB).", - appId, storageUsage, storageAsset.Replicas); + "App {App}: storage usage ({StorageUsage} GB) exceeds the purchased amount ({StorageAsset} GB).", + appId, resourceUsage.Usage, storageAsset.Replicas); await _appDeployService.DestroyAppAllSubscriptionAsync(appId); + return; } } } From cb3f857e444beb5d5918a83acad813804c8cbb2d Mon Sep 17 00:00:00 2001 From: sinclair Date: Wed, 19 Feb 2025 11:20:03 +0800 Subject: [PATCH 9/9] feat: get api --- .../AppResources/Dto/AppResourceUsageDto.cs | 2 +- .../AppResources/IAppResourceUsageService.cs | 1 + .../AppResources/AppResourceUsageService.cs | 11 +++++++++++ src/AeFinder.Application/Apps/AppDeployService.cs | 11 ++++------- .../ScheduledTask/CleanExpiredAssetWorker.cs | 11 ++++------- .../Controllers/AppResourceUsageController.cs | 14 ++++++++++++++ .../AppResources/AppResourceUsageServiceTests.cs | 10 ++++++++++ 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs index 3db201213..edc6a1a5c 100644 --- a/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs +++ b/src/AeFinder.Application.Contracts/AppResources/Dto/AppResourceUsageDto.cs @@ -8,7 +8,7 @@ public class AppResourceUsageDto { public AppInfoImmutable AppInfo { get; set; } public Guid OrganizationId { get; set; } - public Dictionary> ResourceUsages { get; set; } + public Dictionary> ResourceUsages { get; set; } = new(); } public class ResourceUsageDto diff --git a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs index cab931820..1dfbcf2a4 100644 --- a/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs +++ b/src/AeFinder.Application.Contracts/AppResources/IAppResourceUsageService.cs @@ -10,5 +10,6 @@ public interface IAppResourceUsageService { Task AddOrUpdateAsync(List input); Task DeleteAsync(string appId); + Task GetAsync(Guid? organizationId, string appId); Task> GetListAsync(Guid? organizationId, GetAppResourceUsageInput input); } \ No newline at end of file diff --git a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs index cca61ceb7..16755f50d 100644 --- a/src/AeFinder.Application/AppResources/AppResourceUsageService.cs +++ b/src/AeFinder.Application/AppResources/AppResourceUsageService.cs @@ -56,4 +56,15 @@ public async Task> GetListAsync(Guid? organi Items = ObjectMapper.Map, List>(list) }; } + + public async Task GetAsync(Guid? organizationId, string appId) + { + var index = await _entityMappingRepository.GetAsync(appId); + if (index != null && organizationId.HasValue && index.OrganizationId != organizationId.Value) + { + throw new UserFriendlyException("No permission."); + } + + return ObjectMapper.Map(index); + } } \ No newline at end of file diff --git a/src/AeFinder.Application/Apps/AppDeployService.cs b/src/AeFinder.Application/Apps/AppDeployService.cs index 1127be43d..dc5b6bafc 100644 --- a/src/AeFinder.Application/Apps/AppDeployService.cs +++ b/src/AeFinder.Application/Apps/AppDeployService.cs @@ -394,14 +394,11 @@ await appResourceLimitGrain.SetAsync(new SetAppResourceLimitDto() } else { - var appResourceUsage = await _appResourceUsageService.GetListAsync(null, new GetAppResourceUsageInput - { - AppId = appId - }); - - if (appResourceUsage.Items.Any()) + var appResourceUsage = await _appResourceUsageService.GetAsync(null, appId); + + if (appResourceUsage != null) { - foreach (var resourceUsage in appResourceUsage.Items.First().ResourceUsages.Values + foreach (var resourceUsage in appResourceUsage.ResourceUsages.Values .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) { diff --git a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs index 384c73d5c..f45f3341b 100644 --- a/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs +++ b/src/AeFinder.BackgroundWorker.Core/ScheduledTask/CleanExpiredAssetWorker.cs @@ -224,17 +224,14 @@ private async Task> GetAeIndexerAssetListAsync(Guid organizationG private async Task CheckAppStorageAsync(string appId, List appAssets) { - var appResourceUsage = await _appResourceUsageService.GetListAsync(null, new GetAppResourceUsageInput - { - AppId = appId - }); - - if (appResourceUsage.Items.Any()) + var appResourceUsage = await _appResourceUsageService.GetAsync(null, appId); + + if (appResourceUsage!= null) { var storageAsset = appAssets.First(o => o.Status == AssetStatus.Using && o.Merchandise.Type == MerchandiseType.Storage); - foreach (var resourceUsage in appResourceUsage.Items.First().ResourceUsages.Values + foreach (var resourceUsage in appResourceUsage.ResourceUsages.Values .SelectMany(resourceUsages => resourceUsages.Where(resourceUsage => resourceUsage.Name == AeFinderApplicationConsts.AppStorageResourceName))) { diff --git a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs index 7e6caf537..e2e1570c3 100644 --- a/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs +++ b/src/AeFinder.HttpApi/Controllers/AppResourceUsageController.cs @@ -28,6 +28,20 @@ public AppResourceUsageController(IAppResourceUsageService appResourceUsageServi _organizationAppService = organizationAppService; } + [HttpGet] + [Route("{appId}")] + [Authorize] + public async Task GetAsync(string appId) + { + Guid? orgId = null; + if (!CurrentUser.IsInRole(AeFinderApplicationConsts.AdminRoleName)) + { + orgId = await GetOrganizationIdAsync(); + } + + return await _appResourceUsageService.GetAsync(orgId, appId); + } + [HttpGet] [Authorize] public async Task> GetListAsync(GetAppResourceUsageInput input) diff --git a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs index a758a1730..e5b7d61db 100644 --- a/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs +++ b/test/AeFinder.Application.Tests/AppResources/AppResourceUsageServiceTests.cs @@ -11,6 +11,7 @@ using Elasticsearch.Net.Specification.CatApi; using Nest; using Shouldly; +using Volo.Abp; using Xunit; namespace AeFinder.AppResources; @@ -140,5 +141,14 @@ await _appResourceUsageService.AddOrUpdateAsync(new List }); list.Items.Count.ShouldBe(1); list.Items[0].AppInfo.AppId.ShouldBe("app2"); + + var usage = await _appResourceUsageService.GetAsync(AeFinderApplicationTestConsts.OrganizationId, "app1"); + usage.ShouldBeNull(); + + usage = await _appResourceUsageService.GetAsync(AeFinderApplicationTestConsts.OrganizationId, "app2"); + usage.AppInfo.AppId.ShouldBe("app2"); + + await Assert.ThrowsAsync(async () => + await _appResourceUsageService.GetAsync(Guid.NewGuid(), "app2")); } } \ No newline at end of file