1
0
mirror of https://github.com/bitwarden/server synced 2025-12-21 18:53:41 +00:00

[PM-17562] Add in-memory cache for event integrations (#6085)

* [PM-17562] Add in-memory cache for event integrations

* Fix Sql error

* Fix failing test

* Add additional tests for new cache service

* PR suggestions addressed
This commit is contained in:
Brant DeBow
2025-07-16 11:41:08 -04:00
committed by GitHub
parent e9d4403773
commit 5fc7f4700c
16 changed files with 345 additions and 8 deletions

View File

@@ -14,7 +14,7 @@ public class EventIntegrationHandler<T>(
IntegrationType integrationType,
IEventIntegrationPublisher eventIntegrationPublisher,
IIntegrationFilterService integrationFilterService,
IOrganizationIntegrationConfigurationRepository configurationRepository,
IIntegrationConfigurationDetailsCache configurationCache,
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ILogger<EventIntegrationHandler<T>> logger)
@@ -27,7 +27,7 @@ public class EventIntegrationHandler<T>(
return;
}
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
var configurations = configurationCache.GetConfigurationDetails(
organizationId,
integrationType,
eventMessage.Type);

View File

@@ -0,0 +1,73 @@
using System.Diagnostics;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class IntegrationConfigurationDetailsCacheService : BackgroundService, IIntegrationConfigurationDetailsCache
{
private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType EventType);
private readonly IOrganizationIntegrationConfigurationRepository _repository;
private readonly ILogger<IntegrationConfigurationDetailsCacheService> _logger;
private readonly TimeSpan _refreshInterval;
private Dictionary<IntegrationCacheKey, List<OrganizationIntegrationConfigurationDetails>> _cache = new();
public IntegrationConfigurationDetailsCacheService(
IOrganizationIntegrationConfigurationRepository repository,
GlobalSettings globalSettings,
ILogger<IntegrationConfigurationDetailsCacheService> logger
)
{
_repository = repository;
_logger = logger;
_refreshInterval = TimeSpan.FromMinutes(globalSettings.EventLogging.IntegrationCacheRefreshIntervalMinutes);
}
public List<OrganizationIntegrationConfigurationDetails> GetConfigurationDetails(
Guid organizationId,
IntegrationType integrationType,
EventType eventType)
{
var key = new IntegrationCacheKey(organizationId, integrationType, eventType);
return _cache.TryGetValue(key, out var value)
? value
: new List<OrganizationIntegrationConfigurationDetails>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await RefreshAsync();
var timer = new PeriodicTimer(_refreshInterval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshAsync();
}
}
internal async Task RefreshAsync()
{
var stopwatch = Stopwatch.StartNew();
try
{
var newCache = (await _repository.GetAllConfigurationDetailsAsync())
.GroupBy(x => new IntegrationCacheKey(x.OrganizationId, x.IntegrationType, x.EventType))
.ToDictionary(g => g.Key, g => g.ToList());
_cache = newCache;
stopwatch.Stop();
_logger.LogInformation(
"[IntegrationConfigurationDetailsCacheService] Refreshed successfully: {Count} entries in {Duration}ms",
newCache.Count,
stopwatch.Elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
_logger.LogError("[IntegrationConfigurationDetailsCacheService] Refresh failed: {ex}", ex);
}
}
}

View File

@@ -290,6 +290,35 @@ graph TD
C1 -->|Has many| B1_2[IntegrationFilterRule]
C1 -->|Can contain| C2[IntegrationFilterGroup...]
```
## Caching
To reduce database load and improve performance, integration configurations are cached in-memory as a Dictionary
with a periodic load of all configurations. Without caching, each incoming `EventMessage` would trigger a database
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
By loading all configurations into memory on a fixed interval, we ensure:
- Consistent performance for reads.
- Reduced database pressure.
- Predictable refresh timing, independent of event activity.
### Architecture / Design
- The cache is read-only for consumers. It is only updated in bulk by a background refresh process.
- The cache is fully replaced on each refresh to avoid locking or partial state.
- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no
match exists.
- Failures or delays in the loading process do not affect the existing cache state. The cache will continue serving
the last known good state until the update replaces the whole cache.
### Background Refresh
A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the background and:
- Loads all configuration records at application startup.
- Refreshes the cache on a configurable interval.
- Logs timing and entry count on success.
- Logs exceptions on failure without disrupting application flow.
# Building a new integration