Skip to content

fix(plugin): avoid stale Sushi snapshot causing ModelNotFoundException on install#2425

Open
kazaminosuke wants to merge 7 commits into
pelican-dev:mainfrom
kazaminosuke:fix-plugin-install-stale-sushi
Open

fix(plugin): avoid stale Sushi snapshot causing ModelNotFoundException on install#2425
kazaminosuke wants to merge 7 commits into
pelican-dev:mainfrom
kazaminosuke:fix-plugin-install-stale-sushi

Conversation

@kazaminosuke

@kazaminosuke kazaminosuke commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Fixes the root cause behind #2411 / #2415.

Root cause

App\Models\Plugin is a Sushi model backed by a plugins/ directory scan
(getRows()). Sushi only runs this scan once per process (on first boot),
then serves every subsequent query from an in-memory SQLite snapshot for the
rest of that process's lifetime.

AppServiceProvider::register() calls PluginService::loadPlugins() ->
Plugin::orderBy('load_order')->get() during application bootstrap — for
every process, including the long-running queue:work worker.

InstallPlugin/UpdatePlugin/UninstallPlugin all took a Plugin $plugin
constructor argument and relied on SerializesModels to restore it by id
when the worker dequeues the job. That restoration (restoreModel() ->
firstOrFail()) happens before handle() is even called, against the
worker's snapshot from whenever it last booted the Plugin model — so any
plugin added to plugins/ after the worker started is invisible to it,
causing ModelNotFoundException.

This only affects the queue worker (long-lived process); regular web
requests are unaffected since php-fpm boots a fresh process per request.

Fix

  • Added Plugin::refreshRows(), which clears the Eloquent boot guard
    ($booted), the Sushi boot-callback registry ($bootedCallbacks), and
    the cached Sushi connection for this model — forcing a fresh getRows()
    scan the next time the model is touched in the current process.
  • Changed InstallPlugin/UpdatePlugin/UninstallPlugin to take the
    plugin id (string) instead of a Plugin model instance, avoiding
    SerializesModels's restoration entirely. handle() now calls
    Plugin::refreshRows() then Plugin::findOrFail($this->pluginId).
  • Updated the three dispatch() call sites in PluginResource accordingly.

Console commands (e.g. p:plugin:install) call PluginService directly and
synchronously, so they were never affected by this and didn't need changes.

Testing

  • php -l on all changed files: no syntax errors
  • vendor/bin/pint: clean
  • vendor/bin/phpstan analyse (level 6): no errors
  • No existing tests cover Plugin/these jobs, so none were broken

Reproduced and verified on a fresh install with QUEUE_CONNECTION=redis:

  • Before fix (main branch): after worker restart and initial egg-install
    jobs drained, downloading a new plugin and attempting install ->
    InstallPlugin fails in ~15ms with ModelNotFoundException, while
    InstallEgg jobs in the same queue succeed normally.
  • After fix (fix-plugin-install-stale-sushi): same conditions ->
    InstallPlugin completes successfully in ~8s (DONE).

kazaminosuke and others added 6 commits June 21, 2026 17:04
…n on install/update/uninstall

Plugin is a Sushi model whose getRows() scans the plugins/ directory once
per process during boot. Long-running queue workers keep serving that stale
snapshot, so install/update/uninstall jobs that hold a serialized Plugin model
fail at deserialization (restoreModel -> firstOrFail) with
ModelNotFoundException for plugins added after the worker started.

- Add Plugin::refreshRows() to clear the booted flag, the whenBooted boot
  callbacks Sushi registers, and the cached connection, forcing getRows() to
  re-run on next access.
- Make the jobs carry the plugin id (string) instead of the Plugin model and
  re-fetch it in handle() after refreshRows(), so restoration no longer happens
  at deserialization time.
- Pass $plugin->id when dispatching the jobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f95e9a8-c675-4fb3-b92c-0736be4ced72

📥 Commits

Reviewing files that changed from the base of the PR and between 09746b6 and 163397a.

📒 Files selected for processing (1)
  • app/Services/Helpers/PluginService.php

📝 Walkthrough

Walkthrough

Plugin install, update, and uninstall jobs now accept plugin IDs instead of serialized Plugin models. Jobs refresh plugin rows and re-resolve the model at execution time, Plugin gains a Sushi row refresh helper, PluginService reloads the updated plugin before install, and PluginResource dispatches jobs with IDs.

Changes

Plugin ID-based job dispatch and Sushi refresh

Layer / File(s) Summary
Plugin::refreshRows() Sushi reset
app/Models/Plugin.php
Adds a static refreshRows() method that clears Sushi boot state and resets the cached connection so the next model access rebuilds the in-memory snapshot.
PluginService reloads the updated plugin
app/Services/Helpers/PluginService.php
updatePlugin() stores the plugin ID, refreshes rows after download, re-fetches the plugin with findOrFail(), and clears the update cache using the stored ID.
Plugin jobs load by pluginId
app/Jobs/Plugin/InstallPlugin.php, app/Jobs/Plugin/UpdatePlugin.php, app/Jobs/Plugin/UninstallPlugin.php
The three jobs now take pluginId strings, refresh rows in handle(), resolve the plugin at execution time, use the resolved model for service calls and notifications, and key uniqueId() off pluginId.
PluginResource dispatches pluginId
app/Filament/Admin/Resources/Plugins/PluginResource.php
The exclude_install, exclude_update, and exclude_uninstall actions now dispatch jobs with $plugin->id instead of the full plugin model.

Possibly related issues

Possibly related PRs

  • pelican-dev/panel#2267: Introduces the plugin job dispatch flow that this PR refactors to pass plugin IDs instead of serialized models.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. 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 describes the main bug fix: avoiding stale Sushi snapshots that caused ModelNotFoundException on install.
Description check ✅ Passed The description is directly related to the changeset and accurately explains the bug, fix, and validation steps.
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.

@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

🤖 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 `@app/Jobs/Plugin/UpdatePlugin.php`:
- Line 31: The update flow in PluginService::updatePlugin() is using a stale
Plugin instance after the plugin folder is replaced, so refresh/reload the
plugin metadata again after the download completes and before installPlugin()
runs. Update the update path so the Plugin object reflects the new plugin.json
contents before installation, using the existing refresh logic already used in
UpdatePlugin::handle().
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1bcd111d-df32-4323-9fcc-9d7266193b35

📥 Commits

Reviewing files that changed from the base of the PR and between 00a5639 and 09746b6.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/Plugins/PluginResource.php
  • app/Jobs/Plugin/InstallPlugin.php
  • app/Jobs/Plugin/UninstallPlugin.php
  • app/Jobs/Plugin/UpdatePlugin.php
  • app/Models/Plugin.php

Comment thread app/Jobs/Plugin/UpdatePlugin.php
@kazaminosuke kazaminosuke changed the title fix(plugin): avoid stale Sushi snapshot causing ModelNotFoundException on install/update/uninstall fix(plugin): avoid stale Sushi snapshot causing ModelNotFoundException on install Jun 30, 2026
…alling

updatePlugin() downloaded the new version (overwriting plugin.json) but then
ran installPlugin() with the pre-download $plugin instance and stale Sushi
snapshot. As a result installPlugin() read the old version's composer_packages
(and name/category), so newly added composer dependencies were not required
until the snapshot was rebuilt elsewhere (worker restart / next request).

Refresh the snapshot and re-fetch the plugin after the download so both the
$plugin instance and the Plugin::get() inside manageComposerPackages() see the
new plugin.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant