Skip to content

Conversation

@brant-livefront
Copy link
Contributor

📔 Objective

This PR adds 3 new local memory caches to reduce some of the calls to the repository for template parameters. For instance, when an event references a GroupName we need to fetch the group form the database. With this local cache, that will live in a memory cache so frequently hitting the same properties won't require re-loading them.

There are configurable amounts for the max size (number of items) and how many minutes the records should stay in the cache. The TTL is an absolute duration - i.e. if it's set to 30 and you change something, it will take at most 30 minutes to pickup the new change.

These are especially helpful in the Datadog template implementation which makes use of all of these properties and would most likely be used for all events for a given Organization.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@brant-livefront brant-livefront requested a review from a team as a code owner November 7, 2025 18:30
@claude
Copy link
Contributor

claude bot commented Nov 7, 2025

Claude finished @brant-livefront's task —— View job


Code Review Complete

Summary

This PR adds local memory caching for integration template properties (Organization, Group, OrganizationUser lookups) to reduce database calls during event processing. The implementation addresses most previous review findings effectively.

Previous Review Status

Fixed:

  • ✅ Disposal issues (Findings 1 & 2): Removed inappropriate Dispose() methods
  • ✅ Memory leak concern (Finding 6): Now using keyed services for proper disposal
  • ✅ Struct equality (Finding 3): Added == and != operators to OrganizationUserKey
  • ✅ Task.Run overhead (Finding 5): Removed unnecessary ThreadPool scheduling

Not Addressed:

  • ⚠️ Namespace inconsistency (Finding 4): Still exists - see Finding 2 below

New Findings

💭 Finding 1: Null results bypass cache and trigger repeated database calls (IntegrationTemplatePropertyCache.cs:29)

⚠️ Finding 2: Namespace inconsistency - IOrganizationUserUserDetailsCache in different namespace than other cache interfaces (IOrganizationUserUserDetailsCache.cs:3)

🎨 Finding 3: Repetitive cache registration code could be extracted into helper method (ServiceCollectionExtensions.cs:945)

Good Practices Observed

Thread-safe concurrent request handling with AsyncLazy pattern and comprehensive unit tests covering race conditions.

Action Items

  1. Decide on null caching strategy and document the decision (Finding 1)
  2. Fix namespace inconsistency for IOrganizationUserUserDetailsCache (Finding 2)
  3. Consider DRY improvements to cache registration (Finding 3, aligns with @withinfocus feedback)

@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

Logo
Checkmarx One – Scan Summary & Details7ca49f16-9dc2-41f1-89ce-bf9ef81bcc14

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 7, 2025

Codecov Report

❌ Patch coverage is 65.32258% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.79%. Comparing base (1ab2d54) to head (d180029).
⚠️ Report is 46 commits behind head on brant/datadog-template-improvements.

Files with missing lines Patch % Lines
...SharedWeb/Utilities/ServiceCollectionExtensions.cs 0.00% 42 Missing ⚠️
...ntIntegrations/OrganizationUserUserDetailsCache.cs 95.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                           Coverage Diff                           @@
##           brant/datadog-template-improvements    #6553      +/-   ##
=======================================================================
+ Coverage                                52.77%   52.79%   +0.01%     
=======================================================================
  Files                                     1910     1914       +4     
  Lines                                    84890    85001     +111     
  Branches                                  7582     7586       +4     
=======================================================================
+ Hits                                     44800    44875      +75     
- Misses                                   38354    38389      +35     
- Partials                                  1736     1737       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dig it, but want some other eyes on this.

@@ -0,0 +1,64 @@
using System.Collections.Concurrent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 All this is something that I think is so generic and reusable that it could be in a more central / core location, for usage beyond event integrations.

public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings();
public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings();
public int IntegrationCacheRefreshIntervalMinutes { get; set; } = 10;
public int UserCacheTtlMinutes { get; set; } = 60;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Per my other comment, if this is generic perhaps we also rethink how this is loaded and defined.

@withinfocus
Copy link
Contributor

@JimmyVo16 per your recent work on the org cache, something like this albeit with a configurable Redis backing would meet the mark.

{
var loaded = await LoadValueFromRepositoryAsync(key);

if (loaded is not null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Finding 1: Null results bypass cache and trigger repeated database calls

The code only caches non-null values. If LoadValueFromRepositoryAsync returns null (e.g., entity not found), subsequent requests for the same key will repeatedly hit the database.

Impact: Performance degradation for lookups of non-existent entities, which could be common in event processing scenarios.

Options:

  1. Keep current behavior if null results are expected to be rare and should always be fresh
  2. Cache null results with a shorter TTL to prevent repeated DB calls for missing entities
  3. Document this intentional design decision

Is the current null-bypass behavior intentional for this use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@withinfocus I had done this intentionally - none of the things that we are fetching should be null as they are actively referenced on an event. For instance, if the GroupId is set on an event, then that Group should exist. If it had been deleted recently or an edge case like that, my thinking was then we don't need to cache it anyway.

But if we are looking to make this more generic, I wonder if I should make it always cache nulls with a configurable short TTL.

@@ -0,0 +1,8 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;

namespace Bit.Core.Services;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 2: Namespace inconsistency across cache interfaces

This interface is in Bit.Core.Services while the other cache interfaces are in Bit.Core.AdminConsole.Services:

  • IGroupCacheBit.Core.AdminConsole.Services
  • IOrganizationCacheBit.Core.AdminConsole.Services
  • IOrganizationUserUserDetailsCacheBit.Core.Services ← inconsistent

Recommendation: Move this interface to Bit.Core.AdminConsole.Services to match the pattern and improve discoverability.

Previous review finding

This was identified as Finding 4 in the previous review but was not addressed in the recent commits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been fixed - I moved the other cache interfaces to Bit.Core.Services instead.

services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");

services.TryAddKeyedSingleton<IMemoryCache>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Finding 3: Consider extracting cache registration into helper method

The three cache registrations (lines 945-980) follow identical patterns with only names/types differing. This repetition could be reduced.

Suggested refactoring
private static void RegisterPropertyCache<TInterface, TImplementation>(
    this IServiceCollection services,
    string cacheName,
    int maxEntries,
    int ttlMinutes,
    Func<IServiceProvider, IMemoryCache, TimeSpan, TImplementation> factory)
    where TImplementation : TInterface
    where TInterface : class
{
    services.TryAddKeyedSingleton<IMemoryCache>(
        cacheName,
        new MemoryCache(new MemoryCacheOptions { SizeLimit = maxEntries })
    );
    services.TryAddSingleton<TInterface>(provider =>
        factory(
            provider,
            provider.GetRequiredKeyedService<IMemoryCache>(cacheName),
            TimeSpan.FromMinutes(ttlMinutes)
        )
    );
}

// Usage:
services.RegisterPropertyCache<IOrganizationUserUserDetailsCache, OrganizationUserUserDetailsCache>(
    "UserCache",
    globalSettings.EventLogging.UserCacheMaxEntries,
    globalSettings.EventLogging.UserCacheTtlMinutes,
    (p, cache, ttl) => new OrganizationUserUserDetailsCache(
        cache, ttl, p.GetRequiredService<IOrganizationUserRepository>())
);

This aligns with @withinfocus's comment about making this more generic and reusable.

@sonarqubecloud
Copy link

Copy link
Contributor

@r-tome r-tome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! I would be grateful if you could just add some xmldoc to the new interfaces briefly explaining their purpose

@brant-livefront
Copy link
Contributor Author

brant-livefront commented Nov 20, 2025

Closing this in favor of the more robust version in #6608

@brant-livefront brant-livefront deleted the brant/template-property-caching branch November 20, 2025 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants