Fix/remote plugin already loaded- #5749 - #5751
Conversation
…iew maintenance (maximhq#5693) * feat: support matview_refresh_interval "off" to disable logstore matview maintenance The materialized views back only the dashboard UI. Deployments that run Bifrost headless behind their own observability stack pay the REFRESH MATERIALIZED VIEW CONCURRENTLY cost for views nothing reads, and the 5s floor means the interval alone cannot turn maintenance off. With "off" (or a non-positive duration) the logs store skips view creation, the initial refresh, and the periodic refresher entirely. matViewsReady stays false, so dashboard queries fall back to the raw tables, and the runtime self-heal path cannot re-arm maintenance since it only triggers from matview-path queries. * fix: guard matview self-heal when maintenance is disabled Review follow-up: carry the resolved disabled state onto the store so triggerMatViewSelfHeal cannot recreate views the configuration says must not exist, and make the schema/docs explicit that a zero duration also disables (positive sub-5s values still clamp up).
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved token parameter compatibility handling to preserve alternative formats when the primary option is unsupported. * **Chores** * Version updated to 2.0.0. * Enhanced load testing configuration for more reliable builds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary Briefly explain the purpose of this PR and the problem it solves. ## Changes - What was changed and why - Any notable design decisions or trade-offs ## 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 Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [ ] No If yes, describe impact and migration instructions. ## Related issues Link related issues and discussions. Example: Closes maximhq#123 ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Adds a `THIRD_PARTY_NOTICES.md` file to formally document third-party components used in Bifrost that carry license terms requiring explicit attribution — specifically MPL-2.0 licensed dependencies and embedded source code derived from external projects. ## Changes - Introduces `THIRD_PARTY_NOTICES.md` to attribute: - Embedded source code in `framework/migrator/migrator.go` derived from `go-gormigrate/gormigrate` (MIT) - Go binary dependencies carrying MPL-2.0 terms: `github.com/cyphar/filepath-securejoin` and `github.com/hashicorp/go-version` - npm build-time devDependencies carrying MPL-2.0 terms: `lightningcss` (never shipped to end users) and `dompurify` (Apache-2.0 option elected) - All MPL-2.0 components are used unmodified and combined as a "Larger Work" per MPL-2.0 Section 3.3; no Bifrost source files are themselves MPL-licensed. ## Type of change - [ ] Bug fix - [ ] Feature - [ ] Refactor - [x] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [x] Docs ## How to test No functional changes — review the file contents to confirm accuracy of license attributions against the listed upstream repositories. ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations This change has no security implications. It is a legal/compliance attribution document only. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Briefly explain the purpose of this PR and the problem it solves. ## Changes - What was changed and why - Any notable design decisions or trade-offs ## 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 Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [ ] No If yes, describe impact and migration instructions. ## Related issues Link related issues and discussions. Example: Closes maximhq#123 ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Briefly explain the purpose of this PR and the problem it solves. ## Changes - What was changed and why - Any notable design decisions or trade-offs ## 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 Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [ ] No If yes, describe impact and migration instructions. ## Related issues Link related issues and discussions. Example: Closes maximhq#123 ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Briefly explain the purpose of this PR and the problem it solves. ## Changes - What was changed and why - Any notable design decisions or trade-offs ## 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 Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [ ] No If yes, describe impact and migration instructions. ## Related issues Link related issues and discussions. Example: Closes maximhq#123 ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
fix(framework/plugins): prevent "plugin already loaded" on remote plugin update When a custom plugin loaded from a remote URL was updated via the API (e.g. config change, enable/disable), Bifrost re-downloaded the same .so bytes, wrote them to a new temp path, and called plugin.Open again. Go's plugin system treats any plugin binary with the same checksum as already loaded, failing with "plugin already loaded". Fix uses SHA-256 content hash as the stable on-disk filename and an in-memory cache keyed by content hash (not URL): - DownloadPlugin (utils.go): computes SHA-256 while streaming the response body, derives the stable path as /tmp/bifrost-plugin-<hash>.so, and atomically renames the temp file to it. Subsequent downloads of the same content return the existing stable path from cache with no file I/O. - openPlugin (soloader.go): after download, checks the in-memory cache. If cached.loadedPlugin != nil, returns it directly — plugin.Open is skipped entirely. This is the key fix: the same binary is only opened once. - DynamicPlugin (soplugin.go): added contentHash field to track the binary's SHA-256. - Removed 3 tests (soplugin_test.go) that attempted to load the same plugin binary twice in a single test process. This triggers Go's "plugin already loaded" error and is unsupported — plugins are loaded once per process. Fixes maximhq#5741.
📝 WalkthroughSummary by CodeRabbit
WalkthroughRemote plugin downloads now return SHA-256 hashes and deterministic paths. The loader reuses cached paths and plugin instances for identical binaries. Tests cover hashing, path reuse, URL deduplication, different content, and download errors. ChangesRemote plugin deduplication
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant openPlugin
participant DownloadPlugin
participant plugin.Open
participant pluginCache
openPlugin->>DownloadPlugin: download remote plugin
DownloadPlugin->>pluginCache: check content hash and stable path
DownloadPlugin-->>openPlugin: return stable path and content hash
openPlugin->>plugin.Open: open stable plugin path
plugin.Open-->>openPlugin: return loaded plugin
openPlugin->>pluginCache: register loaded plugin
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment Warning |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
framework/plugins/utils.go (3)
160-201: 🚀 Performance & Scalability | 🔵 TrivialStable plugin files under
/tmpare never cleaned up.The comment at Line 154 states these files are intended to persist "from a previous run (e.g. server restart)," and
pluginCachenever evicts entries. Every time a remote plugin's content changes, a newbifrost-plugin-<hash>.sofile accumulates in/tmpalongside all prior versions, for the life of the host (and across restarts, since the design explicitly favors reusing files found on disk). Over a long-running deployment with frequent plugin updates, this grows disk usage without bound.Consider adding a retention policy (for example, keep only the N most recent hashes per plugin
Path, or prune on successful load of a new version) to avoid unbounded disk growth.🤖 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/plugins/utils.go` around lines 160 - 201, Implement retention for stable plugin files created by the download-and-rename flow around setPluginCacheStablePath, so outdated hashes for the same plugin Path are removed while retaining the configured number of recent versions. Prune stale files after successfully caching a new stable path, preserve the current version and reusable files from prior runs, and ignore cleanup failures without disrupting plugin loading.
141-158: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache reuse only skips the disk write, not the network download.
The content hash is computed from
body, which requires the full HTTP download to complete first. The cache check at Line 147 only saves a redundant file write; it does not save the network round trip. If plugin reloads happen frequently (the PR's stated goal is fixing repeated reloads), every reload still re-downloads the full binary from the remote URL even when the content has not changed.There is also no lock around the download-and-write sequence for the same URL or hash. If
openPluginis called concurrently for the same remote plugin (for example, during a bulk config reload), each call downloads and writes independently; the result is still correct because renames are atomic and content is identical, but the work is duplicated.Consider wrapping the fetch in a
singleflight.Groupkeyed bypluginURLto deduplicate concurrent calls, and consider caching by URL with a validator (ETag/Last-Modified) so unchanged plugins skip the network fetch entirely.As per coding guidelines,
**/*.goreview should apply "bounded goroutines/channels, race-safe shared state" practices; deduplicating concurrent downloads for the same key is part of that.🤖 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/plugins/utils.go` around lines 141 - 158, Update openPlugin to deduplicate concurrent fetch-and-write operations with a race-safe singleflight.Group keyed by pluginURL, ensuring only one download occurs per URL while concurrent callers share its result. Add URL-based caching with ETag or Last-Modified validation so unchanged plugins avoid downloading the full body on later reloads, while preserving the existing content-hash stable-path cache behavior.Source: Coding guidelines
20-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove
getPluginCacheHashFromPathunless it is wired into loader logic.The helper is declared, but no code or tests read it. Remove it or connect the loader to the reverse lookup where a stable path needs a content hash.
🤖 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/plugins/utils.go` around lines 20 - 73, Remove the unused getPluginCacheHashFromPath helper from the plugin cache implementation, since no loader logic or tests currently consume the reverse lookup. Leave pluginCacheEntry and the getPluginCacheEntry, setPluginCacheStablePath, and setPluginCacheLoaded flows unchanged.
🤖 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.
Nitpick comments:
In `@framework/plugins/utils.go`:
- Around line 160-201: Implement retention for stable plugin files created by
the download-and-rename flow around setPluginCacheStablePath, so outdated hashes
for the same plugin Path are removed while retaining the configured number of
recent versions. Prune stale files after successfully caching a new stable path,
preserve the current version and reusable files from prior runs, and ignore
cleanup failures without disrupting plugin loading.
- Around line 141-158: Update openPlugin to deduplicate concurrent
fetch-and-write operations with a race-safe singleflight.Group keyed by
pluginURL, ensuring only one download occurs per URL while concurrent callers
share its result. Add URL-based caching with ETag or Last-Modified validation so
unchanged plugins avoid downloading the full body on later reloads, while
preserving the existing content-hash stable-path cache behavior.
- Around line 20-73: Remove the unused getPluginCacheHashFromPath helper from
the plugin cache implementation, since no loader logic or tests currently
consume the reverse lookup. Leave pluginCacheEntry and the getPluginCacheEntry,
setPluginCacheStablePath, and setPluginCacheLoaded flows unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a791178-4ba8-4f14-8d57-5a900feecb36
📒 Files selected for processing (5)
framework/plugins/soloader.goframework/plugins/soplugin.goframework/plugins/soplugin_test.goframework/plugins/utils.goframework/plugins/utils_test.go
|
The PR was postponed after noticing the local build of the Bifrost server didn't look like it had fixed the issue. |
The merge-base changed after approval.
244a01d to
ce1b2a6
Compare
Summary
Fixes a "plugin already loaded" error when updating a custom plugin loaded from a remote URL. When a remote plugin was updated (e.g. config change, enable/disable), Bifrost re-downloaded the same .so bytes, wrote them to a new temp path, and called
plugin.Openagain. Go's plugin system treats any binary with the same checksum as already loaded, failing with "plugin already loaded".Changes
framework/plugins/utils.go:
DownloadPluginnow computes SHA-256 of plugin bytes during download, derives a stable path as/tmp/bifrost-plugin-<hash>.so, and atomically renames the temp file. Subsequent downloads of identical content return the existing stable path from an in-memory cache with no file I/O.framework/plugins/soloader.go: After download,
openPluginchecks the in-memory cache. Ifcached.loadedPlugin != nil, the cached*plugin.Pluginis returned directly —plugin.Openis skipped entirely. This is the core fix: the same binary content is only opened once per process.framework/plugins/soplugin.go: Added
contentHashfield toDynamicPluginto track the binary's SHA-256.framework/plugins/soplugin_test.go: Removed 3 tests (
TestLoadPlugin_CacheHitSkipsOpen,TestLoadPlugin_ConfigUpdateReusesPlugin,TestLoadPlugin_ContentChangeLoadsNewPlugin) that attempted to load the same plugin binary twice in a single test process. Go's plugin system does not support unloading or re-loading a module, so these tests were testing unsupported behavior.Type of change
Affected areas
How to test
Describe the steps to validate this change. Include commands and expected outcomes.
All 23 tests pass. Existing tests (TestLoadPlugins_MultiplePlugins, TestDownloadPlugin_StablePathByContentHash, TestDownloadPlugin_IdenticalContentDifferentURLs) cover the behavior.
Breaking changes
Related issues
Closes #5741
Security considerations
No.
Checklist
docs/contributing/README.mdand followed the guidelines