using System.Collections.Concurrent; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; namespace Bit.Core.AdminConsole.AbilitiesCache; public class VNextInMemoryApplicationCacheService( IOrganizationRepository organizationRepository, IProviderRepository providerRepository, TimeProvider timeProvider) : IVNextInMemoryApplicationCacheService { private ConcurrentDictionary _orgAbilities = new(); private readonly SemaphoreSlim _orgInitLock = new(1, 1); private DateTimeOffset _lastOrgAbilityRefresh = DateTimeOffset.MinValue; private ConcurrentDictionary _providerAbilities = new(); private readonly SemaphoreSlim _providerInitLock = new(1, 1); private DateTimeOffset _lastProviderAbilityRefresh = DateTimeOffset.MinValue; private readonly TimeSpan _refreshInterval = TimeSpan.FromMinutes(10); public virtual async Task> GetOrganizationAbilitiesAsync() { await InitOrganizationAbilitiesAsync(); return _orgAbilities; } public async Task GetOrganizationAbilityAsync(Guid organizationId) { (await GetOrganizationAbilitiesAsync()) .TryGetValue(organizationId, out var organizationAbility); return organizationAbility; } public virtual async Task> GetProviderAbilitiesAsync() { await InitProviderAbilitiesAsync(); return _providerAbilities; } public virtual async Task UpsertProviderAbilityAsync(Provider provider) { await InitProviderAbilitiesAsync(); _providerAbilities.AddOrUpdate( provider.Id, static (_, provider) => new ProviderAbility(provider), static (_, _, provider) => new ProviderAbility(provider), provider); } public virtual async Task UpsertOrganizationAbilityAsync(Organization organization) { await InitOrganizationAbilitiesAsync(); _orgAbilities.AddOrUpdate( organization.Id, static (_, organization) => new OrganizationAbility(organization), static (_, _, organization) => new OrganizationAbility(organization), organization); } public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId) { _orgAbilities.TryRemove(organizationId, out _); return Task.CompletedTask; } public virtual Task DeleteProviderAbilityAsync(Guid providerId) { _providerAbilities.TryRemove(providerId, out _); return Task.CompletedTask; } private async Task InitOrganizationAbilitiesAsync() => await InitAbilitiesAsync( dict => _orgAbilities = dict, () => _lastOrgAbilityRefresh, dt => _lastOrgAbilityRefresh = dt, _orgInitLock, async () => await organizationRepository.GetManyAbilitiesAsync(), _refreshInterval, ability => ability.Id); private async Task InitProviderAbilitiesAsync() => await InitAbilitiesAsync( dict => _providerAbilities = dict, () => _lastProviderAbilityRefresh, dateTime => _lastProviderAbilityRefresh = dateTime, _providerInitLock, async () => await providerRepository.GetManyAbilitiesAsync(), _refreshInterval, ability => ability.Id); private async Task InitAbilitiesAsync( Action> setCache, Func getLastRefresh, Action setLastRefresh, SemaphoreSlim @lock, Func>> fetchFunc, TimeSpan refreshInterval, Func getId) { if (SkipRefresh()) { return; } await @lock.WaitAsync(); try { if (SkipRefresh()) { return; } var sources = await fetchFunc(); var abilities = new ConcurrentDictionary( sources.ToDictionary(getId)); setCache(abilities); setLastRefresh(timeProvider.GetUtcNow()); } finally { @lock.Release(); } bool SkipRefresh() { return timeProvider.GetUtcNow() - getLastRefresh() <= refreshInterval; } } }