Skip to content

[OPIK-7256] fix(backend): make project size-estimate cache actually engage#7371

Open
thiagohora wants to merge 4 commits into
mainfrom
thiagoh/OPIK-7256-fix-project-metadata-cache
Open

[OPIK-7256] fix(backend): make project size-estimate cache actually engage#7371
thiagohora wants to merge 4 commits into
mainfrom
thiagoh/OPIK-7256-fix-project-metadata-cache

Conversation

@thiagohora

@thiagohora thiagohora commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Details

The per-project size estimate (WorkspaceMetadataDAO.getProjectMetadata — a 1000-span size sample plus uniq(id) over the whole project) is meant to be cached, but the cache never engaged, so the query ran on essentially every traces/spans find/search request and became one of the top read-volume queries on the ClickHouse cluster (~65 executions/min). Two stacked defects:

  • @Cacheable(name = "project_metadata", ...) sits on a private method, and Guice method interception (which implements @Cacheable) cannot intercept private methods — the annotation was silently dead. Made the method package-private with a comment explaining why it must not be private.
  • The cache name project_metadata was absent from cacheManager.caches; the config only carried an orphaned workspace_metadata entry that no code references (stale name), so even an intercepted call would have used the 1-second defaultDuration. Renamed the entry to project_metadata (env CACHE_MANAGER_PROJECT_METADATA_DURATION, default PT1H, matching the working experiment_metadata sibling).

With the cache engaged, per-(workspace, project) executions drop from once per request to once per TTL window.

Change checklist

  • User facing
  • Documentation update

Issues

  • Resolves #
  • OPIK-7256

AI-WATERMARK

AI-WATERMARK: yes

  • If yes:
    • Tools: Claude Code
    • Model(s): Claude Fable 5
    • Scope: visibility fix, config rename, commit and PR text
    • Human verification: pending reviewer; production query-frequency numbers measured read-only from system.query_log

Testing

  • Added WorkspaceMetadataServiceCacheTest: boots the app harness with a counting-stub WorkspaceMetadataDAO and asserts a repeated getProjectMetadata call for the same key is served from the cache (DAO hit exactly once), and a different project misses. The test fails if the method becomes non-interceptable again (e.g. private) or if the project_metadata cache name is dropped from configuration.
  • mvn compile and mvn test-compile pass; mvn spotless:apply produced no further changes
  • Guice interception limitation is documented behavior (private methods are never intercepted); the sibling experiment_metadata cache — public method + configured name — demonstrates the working pattern this change aligns with
  • Production system.query_log (7 days): ~657K executions/week of this query with per-request cardinality, confirming the cache was not engaging

🤖 Generated with Claude Code

Documentation

No user-facing documentation impact. The new env var CACHE_MANAGER_PROJECT_METADATA_DURATION (replacing the orphaned CACHE_MANAGER_WORKSPACE_METADATA_DURATION) is documented inline in config.yml.

Note on the sample query itself: we evaluated also reworking the sample CTE's ORDER BY (sorting key) DESC LIMIT 1000 (it reads the whole project because reverse-order read never engages for this shape), but dropping the ORDER BY would pin the sample to the oldest ~1000 spans instead of the latest, so the estimate would stop tracking current span sizes. Keeping the query as-is; with the cache engaged it runs at most once per TTL per project.

…ngage

The @Cacheable on getProjectMetadata never fired: Guice method
interception cannot intercept private methods, and the cache name
project_metadata was absent from cacheManager.caches (only an orphaned
workspace_metadata entry existed), so even an intercepted call would
have fallen back to the 1-second default TTL. The size-estimate query
therefore ran on essentially every traces/spans find request.

- make the method package-private so interception applies
- rename the orphaned workspace_metadata config entry to
  project_metadata (env CACHE_MANAGER_PROJECT_METADATA_DURATION, PT1H)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thiagohora thiagohora requested a review from a team as a code owner July 6, 2026 17:07
@github-actions github-actions Bot added java Pull requests that update Java code Backend labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 5.39s
Total (1 ran) 5.39s
⏭️ 39 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️

Counting-stub DAO behind the real service in the app harness: a second
call with the same key must not reach the DAO. Fails if the method
becomes non-interceptable again (e.g. private) or the cache name is
dropped from configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the tests Including test files, or tests related like configuration. label Jul 6, 2026
…cache entry

The interceptor drops the putAsync completion signal, so poll
CacheManager.contains with a bounded timeout instead of sleeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

All other @Cacheable methods are public; the class stays package-private
so this widens nothing, and the comment keeps the not-private constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend java Pull requests that update Java code tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant