Skip to content

fix: caching logic for external entities - #2155

Merged
yashmehrotra merged 2 commits into
mainfrom
fix-caching-logic
Apr 23, 2026
Merged

fix: caching logic for external entities#2155
yashmehrotra merged 2 commits into
mainfrom
fix-caching-logic

Conversation

@yashmehrotra

@yashmehrotra yashmehrotra commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Refactor

    • Improved external-entity caching for stronger type safety and more efficient cache population.
    • Cache warm-up and write behavior updated to avoid unnecessary expirations and redundant writes.
  • Bug Fixes / Performance

    • Reduced duplicate alias lookups and database queries, improving resolution speed and sync reliability.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Replaces untyped go-cache instances with a generic typedCache[T], converts exported cache globals to concrete typed caches, changes WarmExternalEntityCaches to accept dutycontext.Context, and removes explicit expiration arguments from cache Set calls across external-entity codepaths.

Changes

Cohort / File(s) Summary
Typed cache core & cache helpers
db/external_cache.go
Introduces typedCache[T] and newTypedCache; converts exported caches (OrphanCache, ExternalUserCache, ExternalUserIDCache, ExternalRoleCache, ExternalRoleIDCache, ExternalGroupCache, ExternalGroupIDCache) to *typedCache[...]; updates getEntityCache/getEntityIDCache, findExternalEntityByID, and refactors findAllExternalEntityIDsByAliases (dedupe/skip empty aliases, cache-first lookup, single DB query for misses, populate both id- and alias-caches). Also changes WarmExternalEntityCaches(ctx) signature to accept dutycontext.Context.
Cache writes / callsites
db/external_entities.go, db/external_resolver.go, db/update.go
Removes explicit cache.DefaultExpiration argument from all Set calls; cache writes now use Set(key, value) and write typed values. Update in extractChanges now records orphan marker via OrphanCache.Set(change.ExternalID, true).
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: caching logic for external entities' directly summarizes the main change: refactoring caching logic for external entities by replacing untyped go-cache with typed wrapper and updating cache operations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-caching-logic
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix-caching-logic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown

Benchstat

Base: 63b4842c7f3215599bf715797de1d3191ed77211
Head: 73933f2c4590e309a6ff807f88f1ce95b788af81

📊 2 minor regression(s) (all within 5% threshold)

Benchmark Base Head Change p-value
BenchSaveResultsUpdateChanged/N=1000-4 1.188 1.220 +2.65% 0.041
BenchSaveResultsUpdateChanged/N=1000-4 906.7k 914.8k +0.88% 0.041
✅ 3 improvement(s)
Benchmark Base Head Change p-value
BenchSaveResultsUpdateUnchanged/N=1000-4 749.0m 146.4m -80.45% 0.002
BenchSaveResultsUpdateUnchanged/N=1000-4 315.1k 143.3k -54.53% 0.002
BenchSaveResultsUpdateUnchanged/N=1000-4 25.80Mi 12.49Mi -51.57% 0.002
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/config-db/bench
cpu: AMD EPYC 7763 64-Core Processor                
                                         │ bench-base.txt │           bench-head.txt            │
                                         │     sec/op     │    sec/op     vs base               │
BenchSaveResultsSeed/N=1000-4                621.7m ±  8%   620.3m ±  9%        ~ (p=0.937 n=6)
BenchSaveResultsUpdateUnchanged/N=1000-4     749.0m ±  3%   146.4m ± 14%  -80.45% (p=0.002 n=6)
BenchSaveResultsUpdateChanged/N=1000-4        1.188 ± 15%    1.220 ± 15%   +2.65% (p=0.041 n=6)
geomean                                      821.0m         480.3m        -41.50%

                                         │ bench-base.txt │            bench-head.txt            │
                                         │      MB/s      │    MB/s     vs base                  │
BenchSaveResultsSeed/N=1000-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
BenchSaveResultsUpdateUnchanged/N=1000-4     0.000 ± 0%
BenchSaveResultsUpdateChanged/N=1000-4       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                 ²               +0.00%               ³ ²
¹ all samples are equal
² summaries must be >0 to compute geomean
³ benchmark set differs from baseline; geomeans may not be comparable

                                         │ bench-base.txt │            bench-head.txt            │
                                         │      B/op      │     B/op       vs base               │
BenchSaveResultsSeed/N=1000-4               36.33Mi ±  0%   36.35Mi ±  0%        ~ (p=0.485 n=6)
BenchSaveResultsUpdateUnchanged/N=1000-4    25.80Mi ±  0%   12.49Mi ±  4%  -51.57% (p=0.002 n=6)
BenchSaveResultsUpdateChanged/N=1000-4      75.28Mi ± 11%   75.37Mi ± 11%        ~ (p=0.132 n=6)
geomean                                     41.32Mi         32.47Mi        -21.43%

                                         │ bench-base.txt │           bench-head.txt            │
                                         │   allocs/op    │  allocs/op    vs base               │
BenchSaveResultsSeed/N=1000-4                443.1k ±  0%   443.1k ±  0%        ~ (p=0.589 n=6)
BenchSaveResultsUpdateUnchanged/N=1000-4     315.1k ±  0%   143.3k ±  5%  -54.53% (p=0.002 n=6)
BenchSaveResultsUpdateChanged/N=1000-4       906.7k ± 15%   914.8k ± 14%   +0.88% (p=0.041 n=6)
geomean                                      502.1k         387.3k        -22.88%

                                         │ bench-head.txt │
                                         │      B/s       │
BenchSaveResultsUpdateUnchanged/N=1000-4     9.766Ki ± 0%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
db/external_entities.go (2)

85-115: ⚠️ Potential issue | 🟠 Major

Populate caches with post-merge winner IDs.

This writes cache entries before applying idMap, so a loser ID deleted at Line 87 can be re-added at Line 90 and aliases can point back to merged-away IDs. Cache winner IDs first, and keep loser→winner entries in the ID cache. Apply the same pattern for group/role merge maps if those merge functions can return losers.

🐛 Proposed fix direction
 	if scraperID != nil {
-		for loserID := range idMap {
-			ExternalUserIDCache.Delete(loserID.String())
+		for loserID, winnerID := range idMap {
+			ExternalUserIDCache.Set(loserID.String(), winnerID)
 		}
 		for _, u := range resolvedUsers {
-			ExternalUserIDCache.Set(u.ID.String(), u.ID)
+			winnerID := u.ID
+			if winner, ok := idMap[u.ID]; ok {
+				winnerID = winner
+			}
+			ExternalUserIDCache.Set(winnerID.String(), winnerID)
 			for _, alias := range u.Aliases {
-				ExternalUserCache.Set(alias, u.ID)
+				ExternalUserCache.Set(alias, winnerID)
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/external_entities.go` around lines 85 - 115, The code is populating caches
before applying the idMap rewrites so losers get re-added or aliases point to
merged-away IDs; change the order in the block that updates
ExternalUserIDCache/ExternalUserCache (and the corresponding
ExternalGroup*/ExternalRole* caches) to first rewrite
resolvedUsers/resolvedGroups/resolvedRoles IDs using idMap (the same logic in
the for i := range resolvedUsers loop) and then populate caches using the
post-merge winner IDs; additionally, when processing idMap entries ensure the ID
cache records loser→winner mappings (e.g., set
ExternalUserIDCache.Set(loser.String(), winner)) while setting aliases to the
winner ID so aliases never point to stale loser IDs (apply the same pattern for
resolvedGroups/resolvedRoles and their caches).

703-719: ⚠️ Potential issue | 🟠 Major

Use the merge winner when caching ensured users.

merge_and_upsert_external_users may merge the deterministic id into an existing winner, but the cache still stores id for both ID and alias lookups. That can make follow-up access log resolution use a stale loser ID.

🐛 Proposed fix
 	if err := tx.Commit().Error; err != nil {
 		return fmt.Errorf("failed to commit: %w", err)
 	}
 
-	ExternalUserIDCache.Set(id.String(), id)
+	winnerID := id
+	for _, merge := range merges {
+		if merge.LoserID == id {
+			winnerID = merge.WinnerID
+			break
+		}
+	}
+
+	ExternalUserIDCache.Set(id.String(), winnerID)
+	ExternalUserIDCache.Set(winnerID.String(), winnerID)
 	for _, alias := range aliases {
-		ExternalUserCache.Set(alias, id)
+		ExternalUserCache.Set(alias, winnerID)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/external_entities.go` around lines 703 - 719, The cache is being populated
with the original deterministic id even when merge_and_upsert_external_users
returned a different winner; adjust caching to use the merge winner(s) instead
of the original id by inspecting the merges slice returned from tx.Raw("SELECT *
FROM merge_and_upsert_external_users(?::TEXT)", tempTable) (the merges struct
with LoserID and WinnerID) after commit: for any merge where LoserID == id,
replace id with WinnerID (or build a mapping from loser->winner) and populate
ExternalUserIDCache.Set and ExternalUserCache.Set for aliases using the final
winner ID; also ensure you still populate cache entries for any losers if you
want redirects (map losers to winner) so subsequent lookups resolve to the
winner rather than the stale loser.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@db/external_cache.go`:
- Around line 222-224: aliasCache hits currently mark seen[id] directly, but
idCache stores canonical remappings (ID→winner) so a stale alias can return a
merged-away loser ID and bypass remapping; change the alias-cache branch to take
the id returned from aliasCache.Get(alias), then consult idCache (e.g.,
idCache.Get/Lookup(id)) to obtain the canonical winner ID if present, and mark
seen[winnerID] = true (not the raw alias id) before continuing so alias hits are
canonicalized through idCache.

---

Outside diff comments:
In `@db/external_entities.go`:
- Around line 85-115: The code is populating caches before applying the idMap
rewrites so losers get re-added or aliases point to merged-away IDs; change the
order in the block that updates ExternalUserIDCache/ExternalUserCache (and the
corresponding ExternalGroup*/ExternalRole* caches) to first rewrite
resolvedUsers/resolvedGroups/resolvedRoles IDs using idMap (the same logic in
the for i := range resolvedUsers loop) and then populate caches using the
post-merge winner IDs; additionally, when processing idMap entries ensure the ID
cache records loser→winner mappings (e.g., set
ExternalUserIDCache.Set(loser.String(), winner)) while setting aliases to the
winner ID so aliases never point to stale loser IDs (apply the same pattern for
resolvedGroups/resolvedRoles and their caches).
- Around line 703-719: The cache is being populated with the original
deterministic id even when merge_and_upsert_external_users returned a different
winner; adjust caching to use the merge winner(s) instead of the original id by
inspecting the merges slice returned from tx.Raw("SELECT * FROM
merge_and_upsert_external_users(?::TEXT)", tempTable) (the merges struct with
LoserID and WinnerID) after commit: for any merge where LoserID == id, replace
id with WinnerID (or build a mapping from loser->winner) and populate
ExternalUserIDCache.Set and ExternalUserCache.Set for aliases using the final
winner ID; also ensure you still populate cache entries for any losers if you
want redirects (map losers to winner) so subsequent lookups resolve to the
winner rather than the stale loser.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ee70f00-eaf9-4e0f-9ad0-bfbcf6bd7357

📥 Commits

Reviewing files that changed from the base of the PR and between 63b4842 and 09fd5e6.

📒 Files selected for processing (4)
  • db/external_cache.go
  • db/external_entities.go
  • db/external_resolver.go
  • db/update.go

Comment thread db/external_cache.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@db/external_cache.go`:
- Around line 133-137: Warm-up currently writes empty alias keys into
aliasCache, causing multiple rows with empty aliases to collide; update the loop
that iterates row.Aliases (in db/external_cache.go where rows, row.Aliases are
processed and table.aliasCache.Set is called) to skip empty strings (e.g., if
alias == "" or len(alias) == 0 continue) before calling
table.aliasCache.Set(alias, row.ID) so only non-empty aliases are cached; leave
the table.idCache.Set(row.ID.String(), row.ID) behavior unchanged.
- Around line 31-49: The Set, Delete and Flush methods on typedCache[T]
currently swallow errors from c.inner (used in Set, Delete, Clear) leading to
silent failures; update these methods to surface failures by returning error
(change signatures of typedCache.Set, Delete, Flush to return error) or at
minimum log errors from c.inner.Set/ Delete/ Clear using the project's logger,
and update all callers to handle the new error return; ensure Get continues
returning (T, bool) but other methods either return error or call logger when
c.inner.* returns non-nil, referencing typedCache[T], methods Set/Delete/Flush
and field inner so you can locate and change the implementations consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d45e2622-a4c7-4ac8-8e7f-57978f7ef7b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09fd5e6 and 73933f2.

📒 Files selected for processing (1)
  • db/external_cache.go

Comment thread db/external_cache.go
Comment thread db/external_cache.go
@yashmehrotra
yashmehrotra merged commit 1aae06b into main Apr 23, 2026
18 checks passed
@yashmehrotra
yashmehrotra deleted the fix-caching-logic branch April 23, 2026 07:48
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.

2 participants