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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user