sinelaw/fresh · error
Plugin thread not responding
Error message
Plugin thread not responding
What it means
load_plugin() sends its LoadPlugin request through a bounded channel to the plugin worker thread. This error is raised when `sender.send(...)` fails, which for a channel paired with a oneshot reply means the worker's receiver end has been dropped — the thread is no longer processing requests even though the sender handle still exists. It is distinct from "Plugin thread shut down" (sender is None): here the handle looks alive but the worker is gone.
Solutions
- Verify no code path terminates the plugin thread while loads are in flight; sequence shutdown after all load calls complete.
- Retry once after confirming the runtime is still running — if the thread panicked, restart the plugin thread and re-issue the load.
- Log at the call site whether the runtime was concurrently shut down to find the racy caller.
- Wrap the runtime in an Arc and keep it alive for all callers so the worker isn't dropped mid-request.
Example fix
// before
runtime.load_plugin(&path)?; // may race with shutdown
// after
match runtime.load_plugin(&path) {
Ok(()) => info!("plugin loaded"),
Err(e) if e.to_string().contains("not responding") => {
warn!("plugin thread went away during load; restarting runtime");
runtime.restart_thread()?;
runtime.load_plugin(&path)?;
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: retry
Validate before calling
// Send-probe before real work
fn thread_alive(rt: &PluginRuntimeHandle) -> bool {
// e.g. a cheap status request that round-trips through the channel
rt.ping().is_ok()
} Type guard
fn channel_open(r: &Result<(), SendError<PluginRequest>>) -> bool { r.is_ok() } Try / catch
match rt.load_plugin(&path) {
Err(e) if e.to_string().contains("not responding") => {
warn!("plugin worker dropped receiver; restarting once");
rt.restart_thread()?;
rt.load_plugin(&path)
}
other => other,
} Prevention
- Join the worker thread before assuming the runtime is usable after a previous crash.
- Install a panic hook on the worker so thread death is logged loudly.
- Sequence shutdown after all in-flight loads complete.
- Keep the runtime in an Arc shared by all callers so it cannot be dropped mid-request.
When it happens
Trigger: Calling `load_plugin(path)` at the moment the plugin thread's request receiver has been dropped — typically during or immediately after shutdown, or after the worker loop exited via panic/error while the handle still holds a sender clone.
Common situations: Race between an editor shutdown sequence and a concurrently running plugin loader; a plugin thread crash that closed the channel without clearing the handle's sender; calling into the runtime from another thread right as the worker exits.
Related errors
AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13).
Data as JSON: /api/errors/7c3306b8ea6da7b3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-plugin-runtime/src/thread.rs:579
fire_and_forget(sender.send(PluginRequest::TrackAsyncResource {
plugin_name,
resource,
}));
}
}
}
/// Load a plugin from a file (blocking)
pub fn load_plugin(&self, path: &Path) -> Result<()> {
let (tx, rx) = oneshot::channel();
self.request_sender
.as_ref()
.ok_or_else(|| anyhow!("Plugin thread shut down"))?
.send(PluginRequest::LoadPlugin {
path: path.to_path_buf(),
response: tx,
})
.map_err(|_| anyhow!("Plugin thread not responding"))?;
rx.recv().map_err(|_| anyhow!("Plugin thread closed"))?
}
/// Load all plugins from a directory (blocking)
pub fn load_plugins_from_dir(&self, dir: &Path) -> Vec<String> {
let (tx, rx) = oneshot::channel();
let Some(sender) = self.request_sender.as_ref() else {
return vec!["Plugin thread shut down".to_string()];
};
if sender
.send(PluginRequest::LoadPluginsFromDir {
dir: dir.to_path_buf(),
response: tx,
})
.is_err()
{
return vec!["Plugin thread not responding".to_string()];View on GitHub (pinned to 67894ca546)