Skip to content

feat(metaschema): refresh the schema cache on a timer and make it concurrency-safe - #1878

Merged
rohilsurana merged 6 commits into
mainfrom
feat/metaschema-cache-refresh
Aug 14, 2026
Merged

feat(metaschema): refresh the schema cache on a timer and make it concurrency-safe#1878
rohilsurana merged 6 commits into
mainfrom
feat/metaschema-cache-refresh

Conversation

@rohilsurana

Copy link
Copy Markdown
Member

Summary

Metaschemas hold the JSON schemas that validate entity metadata for users, organizations, groups, and roles. The metaschema service keeps every schema in an in-memory map on the service. Today that map is primed once at boot and after that it only changes on the pod that handled a write. In a multi-pod deployment a schema change made through the API reaches one pod but not the others, so those pods keep validating against the old schema until they restart. The map also has no lock, so concurrent request goroutines on one pod can race on it, which Go turns into a "concurrent map read and map write" panic.

This PR makes the cache safe for concurrent access and reloads it on a timer, so every pod picks up a schema change within a small bounded window.

Changes

  • Guard the schema cache with a sync.RWMutex and switch every service method to a pointer receiver.
  • Add a periodic reload that lists all schemas and swaps the cache, reusing the same robfig/cron pattern as the billing sync jobs.
  • Add Init to prime the cache and start the job, and Close to stop it.
  • Add the app.metaschema.refresh_interval config. It defaults to 1m, and 0 disables the job for single-pod, local, and test runs.
  • Wire Init and Close into server start and shutdown, and document the new setting.

Technical Details

  • Reads (Get, List, Validate) take a read lock. Writes (Create, Update, Delete, and the reload) take a write lock.
  • The writing pod still updates its own cache right away, so its change is visible at once. The timer carries the change to the other pods within one interval.
  • On a reload error the current cache is kept, so a database blip never blanks the schemas.
  • @every schedules from pod start, so pods that start together reload in lockstep. For this tiny table that query cost is small. Jitter or Postgres LISTEN/NOTIFY are noted as future options if it ever matters.
  • The receiver change is internal only. Method signatures do not change, so the MetaSchemaService interface and its generated mock stay valid.

Test Plan

  • Build and type checking passes (go build ./..., go vet)
  • New unit tests in core/metaschema pass under go test -race, covering concurrent access alongside a reload, reload picking up a new schema, cache kept on a list error, and the refresh-disabled path

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 14, 2026 8:10am

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@rohilsurana, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b98e1eba-35a2-4eda-a77b-7d0dc55e15db

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef7235 and 9b062b0.

📒 Files selected for processing (1)
  • cmd/serve.go
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added configurable metaschema cache refreshes, enabled by default every minute.
    • Metaschema data now loads at startup and refreshes automatically in the background.
    • Set the refresh interval to 0 to disable background refreshes while retaining startup loading.
  • Bug Fixes

    • Improved cache safety during concurrent metaschema operations.
    • Preserved existing cached data when a refresh fails.
    • Added validation to reject invalid, non-object schemas with clear errors.
  • Documentation

    • Documented the metaschema refresh configuration and behavior.

Walkthrough

Changes

Metaschema cache lifecycle

Layer / File(s) Summary
Refresh configuration and service wiring
core/metaschema/config.go, pkg/server/config.go, config/sample.config.yaml, docs/content/docs/reference/configurations.mdx, cmd/serve.go, cmd/migrate.go
Adds refresh interval configuration, passes it to service constructors, and manages metaschema initialization and cleanup during server startup.
Synchronized cache lifecycle
core/metaschema/service.go
Adds locked cache access, startup loading, optional periodic refresh, reload error handling, and refresh-job shutdown.
Schema validation and API error mapping
core/metaschema/errors.go, core/metaschema/service.go, internal/api/v1beta1connect/metaschema.go, internal/api/v1beta1connect/metaschema_test.go
Rejects non-object JSON schemas and maps validation failures to Connect InvalidArgument responses.
Cache concurrency and refresh validation
core/metaschema/service_test.go
Tests concurrent access, cache reloads, reload error retention, initialization, and refresh scheduling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 0ef72

The change safely refreshes metaschema data across pods, but shutdown can hang if the service is closed with a non-cancelable context. The PR is otherwise mergeable with explicit owner awareness to cancel the refresh context during Close.

Suggested reviewers: whoabhisheksah

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.

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.

@coveralls

coveralls commented Aug 13, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31782828016

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.2%) to 48.45%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 22 uncovered changes across 3 files (89 of 111 lines covered, 80.18%).
  • 4 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
core/metaschema/service.go 98 85 86.73%
cmd/serve.go 8 0 0.0%
cmd/migrate.go 1 0 0.0%
Total (4 files) 111 89 80.18%

Coverage Regressions

4 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
core/metaschema/service.go 3 71.9%
cmd/serve.go 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 39845
Covered Lines: 19305
Line Coverage: 48.45%
Coverage Strength: 15.55 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana marked this pull request as ready for review August 13, 2026 11:11
@rohilsurana
rohilsurana requested review from AmanGIT07 and whoAbhishekSah and removed request for AmanGIT07 August 13, 2026 11:13

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 59e3f98a-7150-490f-a857-75e9217738c5

📥 Commits

Reviewing files that changed from the base of the PR and between 229ea89 and 3a83e06.

📒 Files selected for processing (8)
  • cmd/migrate.go
  • cmd/serve.go
  • config/sample.config.yaml
  • core/metaschema/config.go
  • core/metaschema/service.go
  • core/metaschema/service_test.go
  • docs/content/docs/reference/configurations.mdx
  • pkg/server/config.go

Comment thread core/metaschema/service.go

@rohilsurana rohilsurana left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Automated review of the metaschema cache refresh. Findings 1 to 4 all trace back to one design choice: reload() does a blind full-cache swap. Left inline below, most serious first.

Comment thread core/metaschema/service.go
Comment thread core/metaschema/service.go
Comment thread core/metaschema/service.go
Comment thread core/metaschema/service.go Outdated
Comment thread core/metaschema/service_test.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.

Caution

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

⚠️ Outside diff range comments (1)
core/metaschema/service.go (1)

182-220: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cancel the refresh context in Close before waiting. Close waits for the cron callback to finish, but Close does not cancel the context captured by Init. Production shutdown cancels its signal context first, but callers that pass context.Background() can block Close indefinitely. Store a cancel function for the refresh context, invoke it in Close, and test cancellation through Repository.List.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ac267f94-49ec-42ad-adb4-eb07fed5244d

📥 Commits

Reviewing files that changed from the base of the PR and between 7201eb2 and 0ef7235.

📒 Files selected for processing (5)
  • core/metaschema/errors.go
  • core/metaschema/service.go
  • core/metaschema/service_test.go
  • internal/api/v1beta1connect/metaschema.go
  • internal/api/v1beta1connect/metaschema_test.go

Comment thread cmd/serve.go Outdated
if schemas, err := deps.MetaSchemaService.List(context.Background()); err != nil {
// prime the metaschema cache and start its periodic refresh
if err := deps.MetaSchemaService.Init(ctx); err != nil {
logger.Warn("metaschemas initialization failed", "err", err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Init returns an error now when the first cache load fails, but the caller only logs a warning and moves on. If the DB is unreachable for a moment at boot, the server comes up with an empty cache and Validate passes everything without checking. With refresh_interval: 0 there is no later reload, so it stays that way until the process restarts. Nothing in the logs says validation is off — just one warn line at startup.

Two ways to close it:

  1. Treat it as a startup failure:
if err := deps.MetaSchemaService.Init(ctx); err != nil {
    return fmt.Errorf("metaschemas initialization: %w", err)
}

If the schemas can't be loaded, the server fails to start, same as a failed migration a few lines below. This also matches the comment inside Init that says "startup stops here" — right now the caller doesn't actually stop.

  1. Fetch on demand — when a read finds the cache empty, fall back to repository.List and fill it. This is what the code did before this PR, so a failed load at boot heals on the first request that needs a schema. The cost is keeping the fallback path this PR deliberately removed, and until that first successful fetch, writes go through without validation.

I'd go with 1 — it's one line and there's no window where validation is silently off.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 9b062b0 with option 1. cmd/serve.go now returns fmt.Errorf("metaschemas initialization: %w", err) when Init fails, so the server stops at startup instead of coming up with an empty cache and validation silently off, same as the failed-migration path just below. Thanks for catching that the caller was not actually stopping.

@rohilsurana
rohilsurana merged commit a2ce258 into main Aug 14, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the feat/metaschema-cache-refresh branch August 14, 2026 08:21
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.

3 participants