Skip to content

feat: move matview refresh out of server pods (--matview-refresh-only, matview_refresh_interval "manual") - #5870

Open
hugochinchilla wants to merge 6 commits into
maximhq:devfrom
anyformat-ai:fix/matview-refresh-out-of-band
Open

hugochinchilla wants to merge 6 commits into
maximhq:devfrom
anyformat-ai:fix/matview-refresh-out-of-band

Conversation

@hugochinchilla

Copy link
Copy Markdown

Summary

Moves the recurring dashboard materialized-view refresh out of the server pods. Every pod refreshes the matviews on its own timer and the refresh gate is per-process, so with steady traffic the refresh load scales with replica count. This adds a way to run that pass once, elsewhere — a CronJob or pg_cron — while the serving pods still create, repair, and use the views.

Note

Stacked on #5300. The commits below #5300's are its --migrate-only / --no-migrate work; this PR's own change is the last commit. Please merge #5300 first, then this rebases to a single commit.

Changes

  • matview_refresh_interval: "manual" — a third mode alongside the "off" that feat: support matview_refresh_interval "off" to disable logstore matview maintenance #5693 shipped, deliberately not a redefinition of it:

    value views created/repaired initial refresh matViewsReady ticker
    "1m" (default) yes yes yes yes
    "off" (feat: support matview_refresh_interval "off" to disable logstore matview maintenance #5693) no no no no
    "manual" (new) yes yes yes no

    "off" means the views must not exist and the read path stays on the raw tables — so an out-of-band refresher alone wouldn't help there, since matViewsReady never flips and queries never use the views. "manual" keeps the views and only drops the ticker.

  • --matview-refresh-only — refreshes all views once and exits, without running migrations or starting the server. Also BIFROST_MATVIEW_REFRESH_ONLY=1 via the Docker entrypoint. Implies --no-migrate: the job runs on a cron against a database the --migrate-only job owns.

  • The index-maintenance switch now checks SkipStartupMigrations() before OneShotMaintenance(), so a matview refresh job doesn't re-run index maintenance on every tick.

  • migrator.MigrateOnlyOneShotMaintenance, RunMigrationsRunMaintenance — both flags now share that path.

  • The sentinel for "manual" is time.Duration(-1), which cannot collide with a parsed value: any non-positive duration already resolves to 0. matViewMaintenanceDisabled is now refreshInterval == 0 rather than <= 0, so self-heal stays armed in manual mode (the views are ours to repair).

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

cd framework && go test ./logstore/... ./migrator/...
cd ../transports && go build ./bifrost-http/ && ./bifrost-http --help | grep matview

TestResolveMatViewRefreshIntervalDefaults pins that "manual" resolves to the sentinel and that no other input — "", a bad string, "1s", "5m", "off", "0s", "-1m" — collides with it.

Against a Postgres logstore:

  1. Set matview_refresh_interval: "manual", start the server. Boot logs show the views created and refreshed once, then periodic matview refresh disabled (matview_refresh_interval=manual). Dashboards serve from the views (no raw-table fallback).
  2. Run bifrost-http --matview-refresh-only. It logs materialized views refreshed in N ms; exiting and returns 0, without applying migrations.
  3. Confirm "off" is unchanged: no views created, dashboard queries fall back to the raw tables.

New config value: matview_refresh_interval: "manual" (documented in docs/deployment-guides/config-json/storage.mdx). New env var: BIFROST_MATVIEW_REFRESH_ONLY. The k8s guide gains a CronJob example.

Breaking changes

  • Yes
  • No

"off" keeps the semantics #5693 shipped; "manual" is additive.

Related issues

Follow-up to #5693. Stacked on #5300.

Security considerations

None. No changes to auth, secrets, or network surface. The refresh job runs with the same config and credentials as the server pods.

@hugochinchilla
hugochinchilla requested a review from a team as a code owner August 5, 2026 16:37
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 45eafa76-57d6-498a-a1fd-69292f6e6038

📥 Commits

Reviewing files that changed from the base of the PR and between 73b0cee and 8066ee8.

📒 Files selected for processing (17)
  • docs/architecture/framework/config-store.mdx
  • docs/deployment-guides/config-json/storage.mdx
  • docs/deployment-guides/k8s.mdx
  • docs/quickstart/gateway/setting-up.mdx
  • framework/changelog.md
  • framework/configstore/migrations.go
  • framework/configstore/migrations_test.go
  • framework/logstore/clickhousemigrate.go
  • framework/logstore/matviews_lock_test.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/migrator/skip.go
  • transports/bifrost-http/main.go
  • transports/bifrost-http/server/server.go
  • transports/changelog.md
  • transports/config.schema.json
  • transports/docker-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (16)
  • framework/logstore/clickhousemigrate.go
  • transports/changelog.md
  • framework/changelog.md
  • docs/architecture/framework/config-store.mdx
  • docs/deployment-guides/k8s.mdx
  • framework/logstore/migrations.go
  • transports/bifrost-http/main.go
  • docs/quickstart/gateway/setting-up.mdx
  • transports/config.schema.json
  • framework/configstore/migrations_test.go
  • framework/logstore/matviews_lock_test.go
  • framework/migrator/skip.go
  • transports/bifrost-http/server/server.go
  • docs/deployment-guides/config-json/storage.mdx
  • framework/configstore/migrations.go
  • framework/logstore/postgres.go

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added controls for startup migration skipping and one-shot migration jobs.
    • Added one-shot materialized-view maintenance and refresh-only operation.
    • Added a manual refresh mode for externally scheduled materialized-view updates.
    • Added safer multi-replica Kubernetes deployment workflows.
  • Bug Fixes

    • Applications now fail fast when pending migrations remain skipped.
    • Improved container argument handling to prevent parsing loops.
  • Documentation

    • Expanded deployment, configuration, quickstart, and changelog guidance.

Walkthrough

The change adds migration lifecycle flags, one-shot maintenance execution, and manual materialized-view refresh mode. Framework stores, the HTTP transport, Docker entrypoint, schemas, tests, changelogs, and deployment documentation now support these controls.

Changes

Migration and maintenance lifecycle

Layer / File(s) Summary
Transport lifecycle controls
transports/bifrost-http/main.go, transports/bifrost-http/server/server.go, transports/docker-entrypoint.sh, transports/changelog.md
The transport parses migration and maintenance flags, runs one-shot maintenance, and exits without starting the HTTP server. The Docker entrypoint maps arguments and environment variables to application flags.
Startup migration enforcement
framework/migrator/skip.go, framework/configstore/migrations.go, framework/logstore/migrations.go, framework/logstore/clickhousemigrate.go, framework/configstore/migrations_test.go, docs/architecture/framework/config-store.mdx, docs/deployment-guides/k8s.mdx, docs/quickstart/gateway/setting-up.mdx, framework/changelog.md
Startup migration triggers skip execution when configured. They return an error when migrations remain pending. Tests cover fresh and migrated databases. Documentation describes one-shot migration jobs and replica startup behavior.
Materialized-view maintenance modes
framework/logstore/postgres.go, framework/logstore/matviews_lock_test.go, transports/config.schema.json, docs/deployment-guides/config-json/storage.mdx, docs/deployment-guides/k8s.mdx
PostgreSQL maintenance adds the manual interval, one-shot execution, reusable index maintenance, and periodic refresh behavior. Schema validation, tests, and Kubernetes guidance document the new mode.

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

Merge Risk: ⚪ Minimal · up to 8066e

The PR adds manual materialized-view refresh and a refresh-only execution mode without a concrete merge-blocking issue; it is merge-ready after normal checks and review.

Suggested reviewers: akshaydeo, impoiler, pratham-mishra04

Sequence Diagram(s)

sequenceDiagram
  participant DockerEntrypoint
  participant BifrostHTTPMain
  participant BifrostHTTPServer
  participant PostgreSQLMaintenance
  DockerEntrypoint->>BifrostHTTPMain: Pass lifecycle flags
  BifrostHTTPMain->>BifrostHTTPServer: RunMaintenance(ctx)
  BifrostHTTPServer->>PostgreSQLMaintenance: Load configuration and run selected maintenance
  PostgreSQLMaintenance-->>BifrostHTTPMain: Return success or error
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 9 files. (8 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: moving materialized-view refreshes out of server pods through manual refresh mode and a refresh-only command.
Description check ✅ Passed The description covers the purpose, implementation changes, affected areas, testing steps, configuration and environment changes, breaking-change status, related issues, and security considerations. T…
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.
Full details: Description check

Explanation

The description covers the purpose, implementation changes, affected areas, testing steps, configuration and environment changes, breaking-change status, related issues, and security considerations. The omitted UI screenshots section is not applicable.

Full details: Docstring Coverage

Explanation

Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 9 files. (8 skipped: 7 unsupported, 1 too large.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 golangci-lint (2.12.2)

Error: can't load config: the Go language version (go1.26) used to build golangci-lint is lower than the targeted Go version (1.27.0)
The command is terminated due to an error: can't load config: the Go language version (go1.26) used to build golangci-lint is lower than the targeted Go version (1.27.0)

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


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

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/deployment-guides/config-json/storage.mdx`:
- Line 282: Update the matview_refresh_interval documentation to clarify that
"manual" preserves the views, performs one create/repair/refresh cycle at server
startup, and then disables recurring server-pod refreshes; retain the reference
to out-of-band refreshes for ongoing maintenance.

In `@framework/logstore/clickhousemigrate.go`:
- Around line 332-337: Update the ClickHouse migration startup flow around
migrator.SkipStartupMigrations() to fail closed unless it can prove all required
migrations are applied. Add or reuse persisted migration-state tracking or a
non-mutating schema-current preflight, and return a descriptive error when
schema currency cannot be verified; do not return nil solely because
--no-migrate is enabled. Verify the migration behavior for framework/** changes.

In `@transports/config.schema.json`:
- Around line 1902-1903: Update the schema pattern used for the matview refresh
interval to accept valid Go duration strings with fractional numeric values and
multiple repeated unit segments, while continuing to allow the existing “off”
and “manual” values. Anchor the change to the pattern alongside the matview
refresh description and keep unsupported formats excluded.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 101b04f4-4cae-44bd-b3df-66ad0ed421e8

📥 Commits

Reviewing files that changed from the base of the PR and between e575bab and b875ca5.

📒 Files selected for processing (17)
  • docs/architecture/framework/config-store.mdx
  • docs/deployment-guides/config-json/storage.mdx
  • docs/deployment-guides/k8s.mdx
  • docs/quickstart/gateway/setting-up.mdx
  • framework/changelog.md
  • framework/configstore/migrations.go
  • framework/configstore/migrations_test.go
  • framework/logstore/clickhousemigrate.go
  • framework/logstore/matviews_lock_test.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/migrator/skip.go
  • transports/bifrost-http/main.go
  • transports/bifrost-http/server/server.go
  • transports/changelog.md
  • transports/config.schema.json
  • transports/docker-entrypoint.sh

| Field | Default | Description |
|-------|---------|-------------|
| `matview_refresh_interval` | `"1m"` | How often to refresh dashboard materialized views. Accepts any Go duration string (`"1m"`, `"5m"`, `"1h"`); positive values below `5s` are clamped up to `5s`. Set `"off"` or a zero duration (`"0s"`) to disable matview maintenance entirely. |
| `matview_refresh_interval` | `"1m"` | How often to refresh dashboard materialized views. Accepts any Go duration string (`"1m"`, `"5m"`, `"1h"`); positive values below `5s` are clamped up to `5s`. Set `"off"` or a zero duration (`"0s"`) to disable matview maintenance entirely. Set `"manual"` to keep the views but refresh them out of band — see [moving materialized view refresh out of server pods](/deployment-guides/k8s#moving-materialized-view-refresh-out-of-server-pods). |

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the startup refresh in "manual" mode.

The current text says that "manual" refreshes views out of band. Server pods still create, repair, and refresh the views once at boot. State that only recurring refreshes move out of band.

As per path instructions, "manual" preserves the views and refreshes them once at boot before disabling recurring server-pod refresh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment-guides/config-json/storage.mdx` at line 282, Update the
matview_refresh_interval documentation to clarify that "manual" preserves the
views, performs one create/repair/refresh cycle at server startup, and then
disables recurring server-pod refreshes; retain the reference to out-of-band
refreshes for ongoing maintenance.

Source: Path instructions

Comment on lines +332 to +337
// No migration ledger exists for ClickHouse, so unlike the SQL stores we
// cannot verify the schema is current — --no-migrate just trusts the
// out-of-band --migrate-only run.
if migrator.SkipStartupMigrations() {
logger.Info("[logstore] --no-migrate set; skipping clickhouse migration run")
return nil

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Fail closed when the ClickHouse schema state is unknown.

Line 335 returns success without checking whether ClickHouse migration steps were applied. A server started with --no-migrate can then use stale ClickHouse tables after a missed or failed --migrate-only job. The SQL stores reject this state, but ClickHouse does not.

Persist applied ClickHouse migration IDs, or add a non-mutating schema-current preflight. Return an error if the process cannot prove that the schema is current. Do not return nil only because SkipStartupMigrations() is true.

Based on PR objectives, --no-migrate must enforce the out-of-band migration workflow. As per path instructions, verify migration behavior for framework/** changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/logstore/clickhousemigrate.go` around lines 332 - 337, Update the
ClickHouse migration startup flow around migrator.SkipStartupMigrations() to
fail closed unless it can prove all required migrations are applied. Add or
reuse persisted migration-state tracking or a non-mutating schema-current
preflight, and return a descriptive error when schema currency cannot be
verified; do not return nil solely because --no-migrate is enabled. Verify the
migration behavior for framework/** changes.

Source: Path instructions

Comment on lines +1902 to +1903
"description": "How often to refresh dashboard materialized views. Go duration string (e.g. '1m', '5m', '1h'). Default 1m. Raise this when matview refresh CPU cost is material on the database instance. Positive values below 5s are clamped up to 5s. Set 'off' or a zero duration (e.g. '0s') to disable materialized-view maintenance entirely; dashboard queries fall back to the raw tables. Set 'manual' to keep the views — created, repaired, and refreshed once at boot — but move the recurring refresh out of the server pods to an out-of-band job (a --matview-refresh-only CronJob, pg_cron).",
"pattern": "^(off|manual|[0-9]+(ns|us|µs|ms|s|m|h))$",

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 'func resolveMatViewRefreshInterval|time\.ParseDuration|matViewRefreshManual' framework/logstore
rg -n -C 2 '"matview_refresh_interval"' transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 7204


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '1890,1912p' transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 2084


Accept fractional and composite Go durations.

resolveMatViewRefreshInterval uses time.ParseDuration(raw), but the schema pattern only allows whole-number single-unit durations. Users can enter valid Go durations such as "1.5s" or "1h30m" and receive the default. Update the pattern to allow fractional numbers and repeated units while keeping "off" and "manual".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transports/config.schema.json` around lines 1902 - 1903, Update the schema
pattern used for the matview refresh interval to accept valid Go duration
strings with fractional numeric values and multiple repeated unit segments,
while continuing to allow the existing “off” and “manual” values. Anchor the
change to the pattern alongside the matview refresh description and keep
unsupported formats excluded.

Sources: Coding guidelines, Path instructions

@CLAassistant

CLAassistant commented Aug 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

hugochinchilla and others added 6 commits August 27, 2026 10:06
… serving

When multiple pods start at once they all race to run schema migrations,
serialized only by an advisory lock with a 1-minute timeout — slow
migrations make sibling pods crash on lock timeout. These flags let
operators run migrations as a one-shot job (--migrate-only) while server
pods boot with --no-migrate, which skips DDL entirely and fails fast with
operator guidance if the schema is not current.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The entrypoint previously hardcoded the args passed to /app/main, so the
new flags were unreachable in the official image; they are now settable
via container args or BIFROST_MIGRATE_ONLY / BIFROST_NO_MIGRATE env vars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catch-all branch rotated unrecognized args (set -- "$@" "$1"; shift),
keeping $# constant and looping forever — hit by the default CMD
/app/main now that parse_args runs for a single argument. Unrecognized
args were never forwarded (exec passes a fixed list), so just drop them.

Reported by greptile on maximhq#5300.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--migrate-only now runs the logstore index maintenance pass (GIN and
performance index builds, dashboard backfill) synchronously before
exiting, instead of spawning the background goroutine and killing it on
Close — which left INVALID indexes behind for the next boot to rebuild.
--no-migrate skips the pass entirely so server pods never contend on
the index advisory lock; the one-shot job owns it. Default
single-process mode keeps the background build unchanged.

Also log a warning when the index lock can't be acquired instead of
silently skipping, and only print "[logstore] pending migrations" when
there actually are pending migrations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every server pod refreshes the dashboard materialized views on its own
timer; the refresh gate is per-process, so with steady traffic the work
scales with replica count. This adds a way to run it once, elsewhere.

"manual" is a third mode alongside the "off" that maximhq#5693 shipped, not a
redefinition of it: "off" means the views must not exist and the read
path stays on the raw tables, while "manual" keeps the views — created,
repaired, and refreshed once at boot, with matViewsReady set — and only
drops the ticker. An out-of-band refresher (a --matview-refresh-only
CronJob, pg_cron) then owns the cadence.

--matview-refresh-only implies --no-migrate: the job runs on a cron
against a database the --migrate-only job owns. The index-maintenance
switch now checks SkipStartupMigrations before OneShotMaintenance so
the refresh job doesn't re-run index maintenance on every tick.

Renames MigrateOnly to OneShotMaintenance and RunMigrations to
RunMaintenance, since both flags now share that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszdWT2EDrVntP5A6q6V3
@hugochinchilla
hugochinchilla force-pushed the fix/matview-refresh-out-of-band branch from b875ca5 to 8066ee8 Compare August 27, 2026 08:12
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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