Skip to content

Fix/remote plugin already loaded- #5749 - #5751

Open
vvidovic wants to merge 8 commits into
maximhq:devfrom
vvidovic:fix/remote-plugin-already-loaded
Open

Fix/remote plugin already loaded- #5749#5751
vvidovic wants to merge 8 commits into
maximhq:devfrom
vvidovic:fix/remote-plugin-already-loaded

Conversation

@vvidovic

@vvidovic vvidovic commented Jul 31, 2026

Copy link
Copy Markdown

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.Open again. Go's plugin system treats any binary with the same checksum as already loaded, failing with "plugin already loaded".

Changes

  • framework/plugins/utils.go: DownloadPlugin now 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, openPlugin checks the in-memory cache. If cached.loadedPlugin != nil, the cached *plugin.Plugin is returned directly — plugin.Open is skipped entirely. This is the core fix: the same binary content is only opened once per process.

  • framework/plugins/soplugin.go: Added contentHash field to DynamicPlugin to 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

  • 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.

cd framework
go test ./plugins/... -v -count=1

All 23 tests pass. Existing tests (TestLoadPlugins_MultiplePlugins, TestDownloadPlugin_StablePathByContentHash, TestDownloadPlugin_IdenticalContentDifferentURLs) cover the behavior.

Breaking changes

  • Yes
  • No

Related issues

Closes #5741

Security considerations

No.

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

jeremym-tanium and others added 8 commits July 31, 2026 00:08
…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.
@CLAassistant

CLAassistant commented Jul 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved remote plugin loading to prevent duplicate-load errors.
    • Reuses identical downloaded plugin binaries instead of downloading or opening them repeatedly.
    • Uses stable, content-based storage paths for downloaded plugins.
    • Added safer handling for download, decompression, and file-storage failures.

Walkthrough

Remote 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.

Changes

Remote plugin deduplication

Layer / File(s) Summary
Download hashing and stable-path cache
framework/plugins/utils.go
DownloadPlugin now returns the stable path and SHA-256 content hash. It caches paths by hash, reuses existing files, and atomically renames downloads.
Loader cache integration
framework/plugins/soloader.go, framework/plugins/soplugin.go
openPlugin reuses cached plugin instances or stable paths. DynamicPlugin stores the content hash.
Hash and path behavior validation
framework/plugins/utils_test.go, framework/plugins/soplugin_test.go
Tests validate hashes, stable paths, cache reuse, URL deduplication, distinct content, error handling, and updated return values.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

  • maximhq/bifrost#5749 — Modifies the same plugin-loading and download-caching code for content-hashed paths.

Suggested reviewers: roroghost17, pratham-mishra04

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title references issue #5749 but describes fixing a 'plugin already loaded' error. The raw_summary and description confirm this is the main change, though the title could be more descriptive about the fix itself.
Description check ✅ Passed The PR description includes all required template sections: Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, Related issues, Security considerations, and Checklist. All critical sections are complete and detailed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments, description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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.

🧹 Nitpick comments (3)
framework/plugins/utils.go (3)

160-201: 🚀 Performance & Scalability | 🔵 Trivial

Stable plugin files under /tmp are never cleaned up.

The comment at Line 154 states these files are intended to persist "from a previous run (e.g. server restart)," and pluginCache never evicts entries. Every time a remote plugin's content changes, a new bifrost-plugin-<hash>.so file accumulates in /tmp alongside 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 win

Cache 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 openPlugin is 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.Group keyed by pluginURL to 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, **/*.go review 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 value

Remove getPluginCacheHashFromPath unless 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

📥 Commits

Reviewing files that changed from the base of the PR and between 29e49c9 and 57cdf5b.

📒 Files selected for processing (5)
  • framework/plugins/soloader.go
  • framework/plugins/soplugin.go
  • framework/plugins/soplugin_test.go
  • framework/plugins/utils.go
  • framework/plugins/utils_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 31, 2026
@vvidovic vvidovic closed this Jul 31, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Aug 3, 2026
18 tasks
@vvidovic

vvidovic commented Aug 3, 2026

Copy link
Copy Markdown
Author

The PR was postponed after noticing the local build of the Bifrost server didn't look like it had fixed the issue.
I created a dynamic build locally using a local framework build, and now it looks good - the problem was in pulling the framework module from web.

@vvidovic vvidovic reopened this Aug 3, 2026
@akshaydeo
akshaydeo dismissed coderabbitai[bot]’s stale review August 5, 2026 07:18

The merge-base changed after approval.

@akshaydeo
akshaydeo requested a review from a team as a code owner August 5, 2026 07:18
@akshaydeo
akshaydeo force-pushed the dev branch 3 times, most recently from 244a01d to ce1b2a6 Compare August 13, 2026 09:47
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.

[Bug]: Updating a custom plugin loaded from a remote URL fails with "plugin already loaded"

6 participants